diff --git a/.github/workflows/deploy-client-docs.yml b/.github/workflows/deploy-client-docs.yml index 36af84d283..9154997513 100644 --- a/.github/workflows/deploy-client-docs.yml +++ b/.github/workflows/deploy-client-docs.yml @@ -16,7 +16,6 @@ on: branches: - '**' paths: - - 'packages/core/client/**' - 'packages/core/client/docs/**' - '.github/workflows/deploy-client-docs.yml' @@ -25,11 +24,12 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: "18" - - run: yarn install + node-version: 18 + cache: 'yarn' + - run: yarn --frozen-lockfile - name: Build zh-CN run: yarn doc build core/client --lang=zh-CN - name: Build en-US diff --git a/.github/workflows/merge.config.ts b/.github/workflows/merge.config.ts new file mode 100644 index 0000000000..6878b85f4a --- /dev/null +++ b/.github/workflows/merge.config.ts @@ -0,0 +1,5 @@ +export default { + // Look for test files in the "tests" directory, relative to this configuration file. + testDir: 'packages', + reporter: [['markdown'], ['html', { outputFolder: `../../e2e-report`, open: 'never' }]] +}; diff --git a/.github/workflows/nocobase-build-test.yml b/.github/workflows/nocobase-build-test.yml index df692d8055..45f56cdab9 100644 --- a/.github/workflows/nocobase-build-test.yml +++ b/.github/workflows/nocobase-build-test.yml @@ -10,29 +10,43 @@ on: - 'main' - 'develop' paths: - - '.github/workflows/nocobase-build-test.yml' - 'packages/**' + - '.github/workflows/nocobase-build-test.yml' pull_request: branches: - '**' paths: - - '.github/workflows/nocobase-build-test.yml' - 'packages/**' + - '.github/workflows/nocobase-build-test.yml' jobs: build-test: - strategy: - matrix: - node_version: [ '18' ] runs-on: ubuntu-latest - container: node:${{ matrix.node_version }} steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node_version }} - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - name: Checkout pro-plugins + continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true + uses: actions/checkout@v4 with: - node-version: ${{ matrix.node_version }} + repository: nocobase/pro-plugins + ref: main + path: packages/pro-plugins + ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 cache: 'yarn' - - run: yarn install + - run: yarn --frozen-lockfile - run: yarn build + env: + __E2E__: true # e2e will be reusing this workflow, so we need to set this flag to true + - uses: actions/upload-artifact@v4 + with: + name: build-artifact + path: | + packages/**/es/ + packages/**/lib/ + packages/**/dist/ + !packages/**/node_modules/** timeout-minutes: 30 diff --git a/.github/workflows/nocobase-e2e.yml b/.github/workflows/nocobase-e2e.yml new file mode 100644 index 0000000000..7ecbf8a72a --- /dev/null +++ b/.github/workflows/nocobase-e2e.yml @@ -0,0 +1,144 @@ +name: E2E without workflows + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_run: + workflows: [Nocobase Build Test] + types: [completed] + +jobs: + e2e-test-postgres: + runs-on: ubuntu-latest + container: node:18 + services: + # Label used to access the service container + postgres: + # Docker Hub image + image: postgres:11 + # Provide the password for postgres + env: + POSTGRES_USER: nocobase + POSTGRES_PASSWORD: password + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v4 + - name: Checkout pro-plugins + continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true + uses: actions/checkout@v4 + with: + repository: nocobase/pro-plugins + ref: main + path: packages/pro-plugins + ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} + - name: Set variables + continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true + run: | + APPEND_PRESET_LOCAL_PLUGINS=$(find ./packages/pro-plugins/@nocobase -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sed 's/^plugin-//' | tr '\n' ',' | sed 's/,$//') + echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT + id: vars + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - uses: actions/cache@v4 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - run: yarn --frozen-lockfile + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: build-artifact + - run: npx playwright install chromium --with-deps + - name: Test with postgres + run: yarn e2e p-test --ignore 'packages/**/{plugin-workflow,plugin-workflow-*}/**/__e2e__/**/*.test.ts' + env: + __E2E__: true + APP_ENV: production + LOGGER_LEVEL: error + DB_DIALECT: postgres + DB_HOST: postgres + DB_PORT: 5432 + DB_USER: nocobase + DB_PASSWORD: password + DB_DATABASE: nocobase + APPEND_PRESET_LOCAL_PLUGINS: ${{ steps.vars.outputs.var2 }} + + - name: Merge reports + run: | + node scripts/moveE2EReportFiles.js && npx playwright merge-reports --config .github/workflows/merge.config.ts ./storage/playwright/tests-report-blob + env: + NODE_OPTIONS: --max-old-space-size=4096 + + - name: Upload e2e-report + uses: actions/upload-artifact@v4 + id: artifact-upload-step + with: + name: e2e-report + path: ./e2e-report/index.html + + - name: Comment on PR + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const jobName = 'E2E without workflows'; + const fs = require('fs'); + const prNumber = '${{ github.event.workflow_run.pull_requests.number }}'; + if (!prNumber) { + core.error('No pull request found for commit ' + context.sha + ' and workflow triggered by: ' + jobName); + return; + } + { + // Mark previous comments as outdated by minimizing them. + const { data: comments } = await github.rest.issues.listComments({ + ...context.repo, + issue_number: prNumber, + }); + for (const comment of comments) { + if (comment.user.login === 'github-actions[bot]' && comment.body.includes(jobName)) { + await github.graphql(` + mutation { + minimizeComment(input: {subjectId: "${comment.node_id}", classifier: OUTDATED}) { + clientMutationId + } + } + `); + } + } + } + const reportUrl = '${{ steps.artifact-upload-step.outputs.artifact-url }}'; + core.notice('Report url: ' + reportUrl); + const mergeWorkflowUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const reportMd = await fs.promises.readFile('report.md', 'utf8'); + function formatComment(lines) { + let body = lines.join('\n'); + if (body.length > 65535) + body = body.substring(0, 65000) + `... ${body.length - 65000} more characters`; + return body; + } + const { data: response } = await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: formatComment([ + `### Test results for "${jobName}"`, + reportMd, + '', + `Full [HTML report](${reportUrl}). Merge [workflow run](${mergeWorkflowUrl}).` + ]), + }); + core.info('Posted comment: ' + response.html_url); + + timeout-minutes: 180 diff --git a/.github/workflows/nocobase-test-e2e.yml b/.github/workflows/nocobase-test-e2e.yml index 93fa629a5a..88558c0f1c 100644 --- a/.github/workflows/nocobase-test-e2e.yml +++ b/.github/workflows/nocobase-test-e2e.yml @@ -21,11 +21,8 @@ on: jobs: e2e-test-postgres: - strategy: - matrix: - node_version: ['18'] runs-on: ubuntu-latest - container: node:${{ matrix.node_version }} + container: node:18 services: # Label used to access the service container postgres: @@ -42,28 +39,26 @@ jobs: --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Checkout pro-plugins - uses: actions/checkout@v3 + continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true + uses: actions/checkout@v4 with: repository: nocobase/pro-plugins ref: main path: packages/pro-plugins ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} - name: Set variables + continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true run: | APPEND_PRESET_LOCAL_PLUGINS=$(find ./packages/pro-plugins/@nocobase -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sed 's/^plugin-//' | tr '\n' ',' | sed 's/,$//') echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT id: vars - - name: Use Node.js ${{ matrix.node_version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node_version }} - cache: 'yarn' + - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -71,7 +66,7 @@ jobs: restore-keys: | ${{ runner.os }}-yarn- - - run: yarn install + - run: yarn --frozen-lockfile - name: yarn build run: yarn build env: @@ -90,4 +85,70 @@ jobs: DB_PASSWORD: password DB_DATABASE: nocobase APPEND_PRESET_LOCAL_PLUGINS: ${{ steps.vars.outputs.var2 }} + + - name: Merge reports + run: | + node scripts/moveE2EReportFiles.js && npx playwright merge-reports --config .github/workflows/merge.config.ts ./storage/playwright/tests-report-blob + env: + NODE_OPTIONS: --max-old-space-size=4096 + + - name: Upload e2e-report + uses: actions/upload-artifact@v4 + id: artifact-upload-step + with: + name: e2e-report + path: ./e2e-report/index.html + + - name: Comment on PR + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const jobName = 'NocoBase E2E Test'; + const fs = require('fs'); + const prNumber = '${{ github.event.pull_request.number }}'; + if (!prNumber) { + core.error('No pull request found for commit ' + context.sha + ' and workflow triggered by: ' + jobName); + return; + } + { + // Mark previous comments as outdated by minimizing them. + const { data: comments } = await github.rest.issues.listComments({ + ...context.repo, + issue_number: prNumber, + }); + for (const comment of comments) { + if (comment.user.login === 'github-actions[bot]' && comment.body.includes(jobName)) { + await github.graphql(` + mutation { + minimizeComment(input: {subjectId: "${comment.node_id}", classifier: OUTDATED}) { + clientMutationId + } + } + `); + } + } + } + const reportUrl = '${{ steps.artifact-upload-step.outputs.artifact-url }}'; + core.notice('Report url: ' + reportUrl); + const mergeWorkflowUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const reportMd = await fs.promises.readFile('report.md', 'utf8'); + function formatComment(lines) { + let body = lines.join('\n'); + if (body.length > 65535) + body = body.substring(0, 65000) + `... ${body.length - 65000} more characters`; + return body; + } + const { data: response } = await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: formatComment([ + `### Test results for "${jobName}"`, + reportMd, + '', + `Full [HTML report](${reportUrl}). Merge [workflow run](${mergeWorkflowUrl}).` + ]), + }); + core.info('Posted comment: ' + response.html_url); + timeout-minutes: 180 diff --git a/.github/workflows/nocobase-workflows-e2e.yml b/.github/workflows/nocobase-workflows-e2e.yml new file mode 100644 index 0000000000..6fdc3ed733 --- /dev/null +++ b/.github/workflows/nocobase-workflows-e2e.yml @@ -0,0 +1,144 @@ +name: Workflows E2E + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_run: + workflows: [Nocobase Build Test] + types: [completed] + +jobs: + e2e-test-postgres: + runs-on: ubuntu-latest + container: node:18 + services: + # Label used to access the service container + postgres: + # Docker Hub image + image: postgres:11 + # Provide the password for postgres + env: + POSTGRES_USER: nocobase + POSTGRES_PASSWORD: password + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v4 + - name: Checkout pro-plugins + continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true + uses: actions/checkout@v4 + with: + repository: nocobase/pro-plugins + ref: main + path: packages/pro-plugins + ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} + - name: Set variables + continue-on-error: true # 外部开发者提交 PR 的时候因为没有权限这里会报错,为了能够继续执行后续步骤,所以这里设置为 continue-on-error: true + run: | + APPEND_PRESET_LOCAL_PLUGINS=$(find ./packages/pro-plugins/@nocobase -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sed 's/^plugin-//' | tr '\n' ',' | sed 's/,$//') + echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT + id: vars + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - uses: actions/cache@v4 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - run: yarn --frozen-lockfile + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: build-artifact + - run: npx playwright install chromium --with-deps + - name: Test with postgres + run: yarn e2e p-test --match 'packages/**/{plugin-workflow,plugin-workflow-*}/**/__e2e__/**/*.test.ts' + env: + __E2E__: true + APP_ENV: production + LOGGER_LEVEL: error + DB_DIALECT: postgres + DB_HOST: postgres + DB_PORT: 5432 + DB_USER: nocobase + DB_PASSWORD: password + DB_DATABASE: nocobase + APPEND_PRESET_LOCAL_PLUGINS: ${{ steps.vars.outputs.var2 }} + + - name: Merge reports + run: | + node scripts/moveE2EReportFiles.js && npx playwright merge-reports --config .github/workflows/merge.config.ts ./storage/playwright/tests-report-blob + env: + NODE_OPTIONS: --max-old-space-size=4096 + + - name: Upload e2e-report + uses: actions/upload-artifact@v4 + id: artifact-upload-step + with: + name: e2e-report + path: ./e2e-report/index.html + + - name: Comment on PR + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const jobName = 'Workflows E2E'; + const fs = require('fs'); + const prNumber = '${{ github.event.workflow_run.pull_requests.number }}'; + if (!prNumber) { + core.error('No pull request found for commit ' + context.sha + ' and workflow triggered by: ' + jobName); + return; + } + { + // Mark previous comments as outdated by minimizing them. + const { data: comments } = await github.rest.issues.listComments({ + ...context.repo, + issue_number: prNumber, + }); + for (const comment of comments) { + if (comment.user.login === 'github-actions[bot]' && comment.body.includes(jobName)) { + await github.graphql(` + mutation { + minimizeComment(input: {subjectId: "${comment.node_id}", classifier: OUTDATED}) { + clientMutationId + } + } + `); + } + } + } + const reportUrl = '${{ steps.artifact-upload-step.outputs.artifact-url }}'; + core.notice('Report url: ' + reportUrl); + const mergeWorkflowUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const reportMd = await fs.promises.readFile('report.md', 'utf8'); + function formatComment(lines) { + let body = lines.join('\n'); + if (body.length > 65535) + body = body.substring(0, 65000) + `... ${body.length - 65000} more characters`; + return body; + } + const { data: response } = await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: formatComment([ + `### Test results for "${jobName}"`, + reportMd, + '', + `Full [HTML report](${reportUrl}). Merge [workflow run](${mergeWorkflowUrl}).` + ]), + }); + core.info('Posted comment: ' + response.html_url); + + timeout-minutes: 180 diff --git a/packages/core/cli/src/commands/e2e.js b/packages/core/cli/src/commands/e2e.js index 72f9ffaa88..8daa5e6389 100644 --- a/packages/core/cli/src/commands/e2e.js +++ b/packages/core/cli/src/commands/e2e.js @@ -148,9 +148,6 @@ const filterArgv = () => { if (element.startsWith('--url=')) { continue; } - if (element === '--skip-reporter') { - continue; - } if (element === '--build') { continue; } @@ -178,7 +175,6 @@ module.exports = (cli) => { .command('test') .allowUnknownOption() .option('--url [url]') - .option('--skip-reporter') .option('--build') .option('--production') .action(async (options) => { @@ -190,9 +186,6 @@ module.exports = (cli) => { process.env.APP_ENV = 'production'; await run('yarn', ['build']); } - if (options.skipReporter) { - process.env.PLAYWRIGHT_SKIP_REPORTER = true; - } if (options.url) { process.env.APP_BASE_URL = options.url.replace('localhost', '127.0.0.1'); } else { diff --git a/packages/core/cli/src/commands/p-test.js b/packages/core/cli/src/commands/p-test.js index 8372dfd494..7145c8aa2e 100644 --- a/packages/core/cli/src/commands/p-test.js +++ b/packages/core/cli/src/commands/p-test.js @@ -43,7 +43,7 @@ async function runApp(dir, index = 0) { await client.query(`DROP DATABASE IF EXISTS "${database}"`); await client.query(`CREATE DATABASE "${database}";`); await client.end(); - return execa('yarn', ['nocobase', 'e2e', 'test', dir, '--skip-reporter'], { + return execa('yarn', ['nocobase', 'e2e', 'test', dir], { shell: true, stdio: 'inherit', env: { @@ -58,6 +58,7 @@ async function runApp(dir, index = 0) { SOCKET_PATH: `storage/e2e/gateway-e2e-${index}.sock`, PM2_HOME: resolve(process.cwd(), `storage/e2e/.pm2-${index}`), PLAYWRIGHT_AUTH_FILE: resolve(process.cwd(), `storage/playwright/.auth/admin-${index}.json`), + E2E_JOB_ID: index, }, }); } diff --git a/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts b/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts index 09d5b64a89..25886da626 100644 --- a/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts +++ b/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts @@ -19,7 +19,7 @@ test.describe('bulk-destroy', () => { await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data')).not.toBeVisible(); // 1. 创建一个批量删除按钮 - await page.getByLabel('schema-initializer-ActionBar-').click(); + await page.getByLabel('schema-initializer-ActionBar-').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); // 2. 选中所有行 @@ -39,7 +39,7 @@ test.describe('bulk-destroy', () => { await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data')).not.toBeVisible(); // 1. 创建一个批量删除按钮,并关闭二次确认 - await page.getByLabel('schema-initializer-ActionBar-').click(); + await page.getByLabel('schema-initializer-ActionBar-').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByLabel('action-Action-Delete-destroy-').hover(); await page.getByLabel('designer-schema-settings-Action-actionSettings:bulkDelete-general').hover(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts index 3ed50b488b..79d8e6281a 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts @@ -702,6 +702,16 @@ export const T3686: PageConfig = { }, ], }, + { + name: 'parentCollection', + fields: [ + { + name: 'parentAssociationField', + interface: 'm2m', + target: 'parentTargetCollection', + }, + ], + }, { name: 'childCollection', inherits: ['parentCollection'], @@ -713,16 +723,6 @@ export const T3686: PageConfig = { }, ], }, - { - name: 'parentCollection', - fields: [ - { - name: 'parentAssociationField', - interface: 'm2m', - target: 'parentTargetCollection', - }, - ], - }, ], pageSchema: { _isJSONSchemaObject: true, diff --git a/packages/core/test/package.json b/packages/core/test/package.json index 9712e1c598..1624724486 100644 --- a/packages/core/test/package.json +++ b/packages/core/test/package.json @@ -52,7 +52,7 @@ "dependencies": { "@faker-js/faker": "8.1.0", "@nocobase/server": "1.0.0-alpha.9", - "@playwright/test": "^1.42.1", + "@playwright/test": "^1.44.0", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.0.0", "@testing-library/react-hooks": "^8.0.1", diff --git a/packages/core/test/src/e2e/defineConfig.ts b/packages/core/test/src/e2e/defineConfig.ts index 547b002d4f..a943080331 100644 --- a/packages/core/test/src/e2e/defineConfig.ts +++ b/packages/core/test/src/e2e/defineConfig.ts @@ -38,9 +38,9 @@ export const defineConfig = (config?: PlaywrightTestConfig) => { maxFailures: 0, // Reporter to use - reporter: process.env.PLAYWRIGHT_SKIP_REPORTER - ? undefined - : [['html', { outputFolder: './storage/playwright/tests-report' }]], + reporter: process.env.CI + ? [['blob', { outputDir: `./storage/playwright/tests-report-blob/blob-${process.env.E2E_JOB_ID}` }]] + : [['html', { outputFolder: `./storage/playwright/tests-report-html`, open: 'never' }]], outputDir: './storage/playwright/test-results', diff --git a/packages/core/test/src/e2e/e2eUtils.ts b/packages/core/test/src/e2e/e2eUtils.ts index e190ba6cb1..31b8ede7d4 100644 --- a/packages/core/test/src/e2e/e2eUtils.ts +++ b/packages/core/test/src/e2e/e2eUtils.ts @@ -9,7 +9,7 @@ import { faker } from '@faker-js/faker'; import { uid } from '@formily/shared'; -import { Page, test as base, expect, request } from '@playwright/test'; +import { Browser, Page, test as base, expect, request } from '@playwright/test'; import _ from 'lodash'; import { defineConfig } from './defineConfig'; @@ -193,6 +193,7 @@ interface CreatePageOptions { } interface ExtendUtils { + page?: Page; /** * 根据配置,生成一个 NocoBase 的页面 * @param pageConfig 页面配置 @@ -304,20 +305,27 @@ export class NocoPage { } async init() { + const waitList = []; if (this.options?.collections?.length) { const collections: any = omitSomeFields(this.options.collections); this.collectionsName = collections.map((item) => item.name); - await createCollections(collections); + waitList.push(createCollections(collections)); } - this.uid = await createPage({ - type: this.options?.type, - name: this.options?.name, - pageSchema: this.options?.pageSchema, - url: this.options?.url, - keepUid: this.options?.keepUid, - }); + waitList.push( + createPage({ + type: this.options?.type, + name: this.options?.name, + pageSchema: this.options?.pageSchema, + url: this.options?.url, + keepUid: this.options?.keepUid, + }), + ); + + const result = await Promise.all(waitList); + + this.uid = result[result.length - 1]; this.url = `${this.options?.basePath || '/admin/'}${this.uid}`; } @@ -340,22 +348,35 @@ export class NocoPage { } async destroy() { + const waitList: any[] = []; if (this.uid) { - await deletePage(this.uid); + waitList.push(deletePage(this.uid)); this.uid = undefined; } if (this.collectionsName?.length) { - await deleteCollections(this.collectionsName); + waitList.push(deleteCollections(this.collectionsName)); this.collectionsName = undefined; } + await Promise.all(waitList); } } +let _page: Page; +const getPage = async (browser: Browser) => { + if (!_page) { + _page = await browser.newPage(); + } + return _page; +}; const _test = base.extend({ - mockPage: async ({ page }, use) => { + page: async ({ browser }, use) => { + await use(await getPage(browser)); + }, + mockPage: async ({ browser }, use) => { // 保证每个测试运行时 faker 的随机值都是一样的 // faker.seed(1); + const page = await getPage(browser); const nocoPages: NocoPage[] = []; const mockPage = (config?: PageConfig) => { const nocoPage = new NocoPage(config, page); @@ -365,13 +386,18 @@ const _test = base.extend({ await use(mockPage); + const waitList = []; + // 测试运行完自动销毁页面 for (const nocoPage of nocoPages) { + // 这里之所以不加入 waitList 是因为会导致 acl 的测试报错 await nocoPage.destroy(); - await setDefaultRole('root'); } + waitList.push(setDefaultRole('root')); // 删除掉 id 不是 1 的 users 和 name 不是 root admin member 的 roles - await removeRedundantUserAndRoles(); + waitList.push(removeRedundantUserAndRoles()); + + await Promise.all(waitList); }, mockManualDestroyPage: async ({ browser }, use) => { const mockManualDestroyPage = (config?: PageConfig) => { @@ -381,7 +407,7 @@ const _test = base.extend({ await use(mockManualDestroyPage); }, - createCollections: async ({ page }, use) => { + createCollections: async ({ browser }, use) => { let collectionsName: string[] = []; const _createCollections = async (collectionSettings: CollectionSetting | CollectionSetting[]) => { @@ -398,7 +424,7 @@ const _test = base.extend({ await deleteCollections(_.uniq(collectionsName)); } }, - mockCollections: async ({ page }, use) => { + mockCollections: async ({ browser }, use) => { let collectionsName: string[] = []; const destroy = async () => { if (collectionsName.length) { @@ -415,7 +441,7 @@ const _test = base.extend({ await use(mockCollections); await destroy(); }, - mockCollection: async ({ page }, use) => { + mockCollection: async ({ browser }, use) => { let collectionsName: string[] = []; const destroy = async () => { if (collectionsName.length) { @@ -432,7 +458,7 @@ const _test = base.extend({ await use(mockCollection); await destroy(); }, - mockRecords: async ({ page }, use) => { + mockRecords: async ({ browser }, use) => { const mockRecords = async (collectionName: string, count: any = 3, data?: any) => { let maxDepth: number; if (_.isNumber(data)) { @@ -448,7 +474,7 @@ const _test = base.extend({ await use(mockRecords); }, - mockRecord: async ({ page }, use) => { + mockRecord: async ({ browser }, use) => { const mockRecord = async (collectionName: string, data?: any, maxDepth?: any) => { if (_.isNumber(data)) { maxDepth = data; @@ -461,7 +487,9 @@ const _test = base.extend({ await use(mockRecord); }, - deletePage: async ({ page }, use) => { + deletePage: async ({ browser }, use) => { + const page = await getPage(browser); + const deletePage = async (pageName: string) => { await page.getByText(pageName, { exact: true }).hover(); await page.getByRole('button', { name: 'designer-schema-settings-' }).hover(); @@ -471,18 +499,14 @@ const _test = base.extend({ await use(deletePage); }, - mockRole: async ({ page }, use) => { + mockRole: async ({ browser }, use) => { const mockRole = async (roleSetting: AclRoleSetting) => { return createRole(roleSetting); }; await use(mockRole); }, - updateRole: async ({ page }, use) => { - async (roleSetting: AclRoleSetting) => { - return updateRole(roleSetting); - }; - + updateRole: async ({ browser }, use) => { await use(updateRole); }, mockExternalDataSource: async ({ browser }, use) => { @@ -499,7 +523,7 @@ const _test = base.extend({ await use(destoryDataSource); }, - clearBlockTemplates: async ({ page }, use) => { + clearBlockTemplates: async ({ browser }, use) => { const clearBlockTemplates = async () => { const api = await request.newContext({ storageState: process.env.PLAYWRIGHT_AUTH_FILE, @@ -811,11 +835,16 @@ const setDefaultRole = async (name) => { }); const state = await api.storageState(); const headers = getHeaders(state); - await api.post(`/api/users:setDefaultRole`, { + const result = await api.post(`/api/users:setDefaultRole`, { headers, data: { roleName: name }, }); + + if (!result.ok()) { + throw new Error(await result.text()); + } }; + /** * 创建外部数据源 * @paramn @@ -834,8 +863,7 @@ const createExternalDataSource = async (dataSourceSetting: DataSourceSetting) => if (!result.ok()) { throw new Error(await result.text()); } - const dataSourceData = (await result.json()).data; - return dataSourceData; + return (await result.json()).data; }; /** @@ -855,8 +883,7 @@ const destoryExternalDataSource = async (key) => { if (!result.ok()) { throw new Error(await result.text()); } - const dataSourceData = (await result.json()).data; - return dataSourceData; + return (await result.json()).data; }; /** * 根据 collection 的配置生成 Faker 数据 diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/fields/manyToMany/schemaInitializer.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/fields/manyToMany/schemaInitializer.test.ts deleted file mode 100644 index d3436f53a5..0000000000 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/fields/manyToMany/schemaInitializer.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - diff --git a/packages/plugins/@nocobase/plugin-mock-collections/src/server/index.ts b/packages/plugins/@nocobase/plugin-mock-collections/src/server/index.ts index cd5ed2ea14..d3d3d5d044 100644 --- a/packages/plugins/@nocobase/plugin-mock-collections/src/server/index.ts +++ b/packages/plugins/@nocobase/plugin-mock-collections/src/server/index.ts @@ -196,7 +196,7 @@ export class PluginMockCollectionsServer extends Plugin { return options; }; - this.app.resourcer.registerActions({ + this.app.resourceManager.registerActionHandlers({ mock: async (ctx, next) => { const { resourceName } = ctx.action; const { values, count = 10, maxDepth = 4 } = ctx.action.params; @@ -228,7 +228,7 @@ export class PluginMockCollectionsServer extends Plugin { ); return count == 1 ? items[0] : items; }; - const repository = ctx.db.getRepository(resourceName); + const repository = ctx.db.getRepository(resourceName) as CollectionRepository; let size = count; if (Array.isArray(values)) { size = values.length; @@ -302,9 +302,12 @@ export class PluginMockCollectionsServer extends Plugin { } } }); - await collectionsRepository.load(); - await db.sync(); + + for (const collection of collections) { + await db.getRepository(collection.name).collection.sync(); + } + const records = await collectionsRepository.find({ filter: { name: collections.map((c) => c.name), diff --git a/scripts/moveE2EReportFiles.js b/scripts/moveE2EReportFiles.js new file mode 100644 index 0000000000..27e250297c --- /dev/null +++ b/scripts/moveE2EReportFiles.js @@ -0,0 +1,31 @@ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +// 源路径和目标路径 +const sourcePattern = './storage/playwright/tests-report-blob/blob-*/*'; +const targetDir = './storage/playwright/tests-report-blob/'; + +// 确保目标目录存在 +fs.mkdirSync(targetDir, { recursive: true }); + +// 使用 glob 模块匹配文件 +glob(sourcePattern, (err, files) => { + if (err) { + console.error('Error matching files:', err); + return; + } + + // 移动每个文件 + files.forEach((file) => { + const targetFile = path.join(targetDir, path.basename(file)); + + fs.rename(file, targetFile, (err) => { + if (err) { + console.error(`Error moving file ${file}:`, err); + } else { + console.log(`Moved file ${file} to ${targetDir}`); + } + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 0b56b3321d..59ed203777 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4954,12 +4954,12 @@ picocolors "^1.0.0" tslib "^2.6.0" -"@playwright/test@^1.42.1": - version "1.42.1" - resolved "https://registry.npmmirror.com/@playwright/test/-/test-1.42.1.tgz#9eff7417bcaa770e9e9a00439e078284b301f31c" - integrity sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ== +"@playwright/test@^1.44.0": + version "1.44.0" + resolved "https://registry.npmmirror.com/@playwright/test/-/test-1.44.0.tgz#ac7a764b5ee6a80558bdc0fcbc525fcb81f83465" + integrity sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg== dependencies: - playwright "1.42.1" + playwright "1.44.0" "@pm2/agent@~2.0.0": version "2.0.3" @@ -20664,17 +20664,17 @@ platform@^1.3.1: resolved "https://registry.npmmirror.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== -playwright-core@1.42.1: - version "1.42.1" - resolved "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.42.1.tgz#13c150b93c940a3280ab1d3fbc945bc855c9459e" - integrity sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA== +playwright-core@1.44.0: + version "1.44.0" + resolved "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.44.0.tgz#316c4f0bca0551ffb88b6eb1c97bc0d2d861b0d5" + integrity sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ== -playwright@1.42.1: - version "1.42.1" - resolved "https://registry.npmmirror.com/playwright/-/playwright-1.42.1.tgz#79c828b51fe3830211137550542426111dc8239f" - integrity sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg== +playwright@1.44.0: + version "1.44.0" + resolved "https://registry.npmmirror.com/playwright/-/playwright-1.44.0.tgz#22894e9b69087f6beb639249323d80fe2b5087ff" + integrity sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ== dependencies: - playwright-core "1.42.1" + playwright-core "1.44.0" optionalDependencies: fsevents "2.3.2"