nuxt-spec 0.3.4 → 0.4.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +13 -13
  3. package/app/app.vue +10 -10
  4. package/app/components/NuxtSpecApiTestComponent.vue +24 -24
  5. package/app/components/NuxtSpecTestComponent.vue +9 -9
  6. package/app/components/index.d.ts +7 -7
  7. package/app/components/index.ts +2 -2
  8. package/app/utils/vitest-utils.ts +5 -5
  9. package/bin/_internal/prepublish-check.js +16 -16
  10. package/bin/cli.js +53 -44
  11. package/bin/commands/ci.js +124 -0
  12. package/bin/commands/setup.js +367 -0
  13. package/bin/commands/update.js +126 -0
  14. package/bin/helpers/commands.js +82 -82
  15. package/bin/helpers/constants.js +8 -0
  16. package/bin/helpers/git.js +73 -0
  17. package/config/index.d.ts +36 -33
  18. package/config/index.mjs +147 -132
  19. package/config/report/report-entry.html +6 -0
  20. package/config/report/report-head.html +34 -0
  21. package/{utils/screenshot/report-entry.html → config/report/report-screenshot.html} +8 -8
  22. package/config/report/report-tail.html +4 -0
  23. package/config/templates/.github/workflows/ci-test-update.yml.template +105 -0
  24. package/config/templates/.github/workflows/ci-test-verify.yml.template +68 -0
  25. package/config/templates/.gitlab/ci/ci-test-update.yml.template +69 -0
  26. package/config/templates/.gitlab/ci/ci-test-verify.yml.template +41 -0
  27. package/config/templates/.gitlab-ci.yml.template +6 -0
  28. package/config/templates/pnpm-workspace.yaml.template +19 -19
  29. package/config/templates/vitest.config.ts.template +5 -5
  30. package/config/utils/merge.mjs +43 -43
  31. package/config/utils/options.d.mts +59 -0
  32. package/config/utils/options.mjs +116 -0
  33. package/config/utils/reporter.mjs +409 -0
  34. package/config/utils/warnings.mjs +38 -33
  35. package/modules/spec-options.ts +23 -0
  36. package/nuxt.config.ts +20 -24
  37. package/package.json +37 -31
  38. package/utils/e2e.ts +131 -131
  39. package/utils/helpers/check-params.ts +44 -41
  40. package/utils/helpers/location-hint.ts +30 -30
  41. package/utils/helpers/screenshot-utils.ts +27 -0
  42. package/utils/index.ts +12 -12
  43. package/utils/playwright.ts +28 -20
  44. package/utils/screenshot.ts +201 -172
  45. package/bin/setup.js +0 -287
  46. package/bin/update.js +0 -94
  47. package/utils/screenshot/report-head.html +0 -24
  48. package/utils/screenshot/report-tail.html +0 -4
  49. package/utils/screenshot/report-utils.ts +0 -236
@@ -0,0 +1,73 @@
1
+ import { execFileSync } from 'node:child_process'
2
+
3
+ export const FALLBACK_DEFAULT_BRANCH = 'main'
4
+
5
+ // run a read-only git command, returning trimmed stdout or `undefined` on failure
6
+ function git(...args) {
7
+ try {
8
+ return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || undefined
9
+ } catch {
10
+ return undefined
11
+ }
12
+ }
13
+
14
+ function hasRef(ref) {
15
+ try {
16
+ execFileSync('git', ['show-ref', '--verify', '--quiet', ref], { stdio: 'ignore' })
17
+ return true
18
+ } catch {
19
+ return false
20
+ }
21
+ }
22
+
23
+ export function isGitRepo() {
24
+ return git('rev-parse', '--is-inside-work-tree') === 'true'
25
+ }
26
+
27
+ /**
28
+ * Resolve the default branch of the Git repository in the current working directory.
29
+ *
30
+ * Tries (in order):
31
+ * 1) HEAD of `origin` remote (or the first available remote) - set when cloned or via `git remote set-head <remote> --auto`
32
+ * 2) existing local or remote-tracking `main` or `master` branch
33
+ * 3) the only local branch, or current branch if there are no commits yet
34
+ * 4) `init.defaultBranch` from Git config
35
+ *
36
+ * @returns {{ branch: string, source: string }} resolved branch name and description of where it came from
37
+ */
38
+ export function resolveDefaultBranch() {
39
+ if (isGitRepo()) {
40
+ const remotes = git('remote')?.split(/\r?\n/) ?? []
41
+ const remote = remotes.includes('origin') ? 'origin' : remotes[0]
42
+ if (remote) {
43
+ const remoteHead = git('symbolic-ref', '--quiet', '--short', `refs/remotes/${remote}/HEAD`)
44
+ if (remoteHead?.startsWith(`${remote}/`)) {
45
+ return { branch: remoteHead.slice(remote.length + 1), source: `HEAD of '${remote}' remote` }
46
+ }
47
+ }
48
+
49
+ for (const candidate of ['main', 'master']) {
50
+ if (hasRef(`refs/heads/${candidate}`) || (remote && hasRef(`refs/remotes/${remote}/${candidate}`))) {
51
+ return { branch: candidate, source: 'existing branch' }
52
+ }
53
+ }
54
+
55
+ const localBranches = git('for-each-ref', '--format=%(refname:short)', 'refs/heads')?.split(/\r?\n/) ?? []
56
+ if (localBranches.length === 1) {
57
+ return { branch: localBranches[0], source: 'the only local branch' }
58
+ }
59
+ if (localBranches.length === 0) {
60
+ const unbornHead = git('symbolic-ref', '--quiet', '--short', 'HEAD')
61
+ if (unbornHead) {
62
+ return { branch: unbornHead, source: 'current branch of repository without commits' }
63
+ }
64
+ }
65
+ }
66
+
67
+ const configured = git('config', '--get', 'init.defaultBranch')
68
+ if (configured) {
69
+ return { branch: configured, source: 'Git config \'init.defaultBranch\'' }
70
+ }
71
+
72
+ return { branch: FALLBACK_DEFAULT_BRANCH, source: 'fallback value' }
73
+ }
package/config/index.d.ts CHANGED
@@ -1,33 +1,36 @@
1
- import type { UserConfig } from 'vite'
2
- import type { TestUserConfig } from 'vitest/config'
3
-
4
- /** Typed Vite configuration object for tests */
5
- type ExtendedUserConfig = UserConfig & {
6
- test?: TestUserConfig
7
- }
8
-
9
- /**
10
- * Allows fine-grained control over default Vitest `projects`.
11
- *
12
- * By default, everything is included.
13
- * If set to `false`, `test.projects` key is not merged at all.
14
- * If set to object, specific projects can be omitted by setting respective key to `false`.
15
- */
16
- type ProjectsConfig = boolean | {
17
- default?: boolean
18
- unit?: boolean
19
- nuxt?: boolean
20
- e2e?: boolean
21
- browser?: boolean
22
- }
23
-
24
- /**
25
- * Prepare Vitest configuration object - user config merged with nuxt-spec defaults
26
- * @param userVitestConfig - custom Vitest config passed from the user
27
- * @param projects - allows fine-grained control over default `test.projects` - see `ProjectsConfig` for details
28
- * @returns Promise resolving to defu-merged Vitest configuration
29
- */
30
- export declare function loadVitestConfig(
31
- userVitestConfig: ExtendedUserConfig,
32
- projects?: ProjectsConfig,
33
- ): Promise<ExtendedUserConfig>
1
+ import type { UserConfig } from 'vite'
2
+ import type { TestUserConfig } from 'vitest/config'
3
+
4
+ export type { HtmlReportOpenMode, NuxtSpecOptions } from './utils/options.mjs'
5
+
6
+ /** Typed Vite configuration object for tests */
7
+ type ExtendedUserConfig = UserConfig & {
8
+ test?: TestUserConfig
9
+ }
10
+
11
+ /**
12
+ * Allows fine-grained control over default Vitest `projects`.
13
+ *
14
+ * By default, everything is included.
15
+ * If set to `false`, `test.projects` key is not merged at all.
16
+ * If set to object, specific projects can be omitted by setting respective key to `false`.
17
+ */
18
+ type ProjectsConfig = boolean | {
19
+ default?: boolean
20
+ unit?: boolean
21
+ nuxt?: boolean
22
+ e2e?: boolean
23
+ browser?: boolean
24
+ }
25
+
26
+ /**
27
+ * Prepare Vitest configuration object - user config merged with nuxt-spec defaults
28
+ * (defaults are adjusted by `spec` key from nuxt.config.ts and NUXT_SPEC_* env variables)
29
+ * @param userVitestConfig - custom Vitest config passed from the user
30
+ * @param projects - allows fine-grained control over default `test.projects` - see `ProjectsConfig` for details
31
+ * @returns Promise resolving to defu-merged Vitest configuration
32
+ */
33
+ export declare function loadVitestConfig(
34
+ userVitestConfig: ExtendedUserConfig,
35
+ projects?: ProjectsConfig,
36
+ ): Promise<ExtendedUserConfig>
package/config/index.mjs CHANGED
@@ -1,132 +1,147 @@
1
- // this is the default Vitest config object
2
- // based on https://nuxt.com/docs/4.x/getting-started/testing#setup
3
- // `projects=false` can be used to suspend the default usage of "projects" in Vitest config
4
-
5
- import { availableParallelism } from 'node:os'
6
- import { fileURLToPath } from 'node:url'
7
- import { onConsoleLog } from './utils/warnings.mjs' // filter out unnecessary logs
8
- import { mergeConfig } from './utils/merge.mjs' // defu-based merge function
9
- import { defineConfig } from 'vitest/config'
10
- import { defineVitestProject } from '@nuxt/test-utils/config'
11
- import { playwright } from '@vitest/browser-playwright'
12
- import vue from '@vitejs/plugin-vue'
13
-
14
- // absolute paths so it works from within the nuxt-ignis package
15
- const screenshotReportSetup = fileURLToPath(new URL('../utils/screenshot.ts', import.meta.url))
16
- const externalPlaywrightSetup = fileURLToPath(new URL('../utils/playwright.ts', import.meta.url))
17
-
18
- // external Playwright instance (if configured)
19
- const externalPlaywright = process.env.NUXT_SPEC_EXTERNAL_PLAYWRIGHT
20
-
21
- export async function loadVitestConfig(userVitestConfig, projects = true) {
22
- const baseConfig = {
23
- test: {
24
- // filter-out unnecessary console logs coming from Vitest
25
- // when the import is resolved, unnecessary stderr logs are also filtered-out
26
- // as a side-effect
27
- onConsoleLog,
28
- // Vitest defaults to 5 which might be unnecessarily restrictive
29
- // if the host machine can support more
30
- maxConcurrency: availableParallelism() / 2,
31
- },
32
- }
33
-
34
- // add proposed projects settings
35
- // user can control the inclusion via `projects: ProjectsConfig` config object
36
- // projects are included by default unless not explicitly disabled
37
- if (projects !== false) {
38
- baseConfig.test.projects = []
39
-
40
- // default fallback to catch tests directly in /test folder
41
- if (projects.default !== false) {
42
- baseConfig.test.projects.push({
43
- extends: true,
44
- test: {
45
- name: 'default',
46
- include: ['{test,tests}/**/*.{test,spec}.ts', '!test/{browser,e2e,nuxt,unit}/**'],
47
- environment: 'node',
48
- },
49
- })
50
- }
51
-
52
- // proposed setup for Unit tests
53
- if (projects.unit !== false) {
54
- baseConfig.test.projects.push({
55
- extends: true,
56
- test: {
57
- name: 'unit',
58
- include: ['test/unit/**/*.{test,spec}.ts'],
59
- environment: 'node',
60
- },
61
- })
62
- }
63
-
64
- // proposed setup for Nuxt component tests
65
- if (projects.nuxt !== false) {
66
- baseConfig.test.projects.push(
67
- await defineVitestProject({
68
- extends: true,
69
- test: {
70
- name: 'nuxt',
71
- include: ['test/nuxt/**/*.{test,spec}.ts'],
72
- environment: 'nuxt',
73
- },
74
- }),
75
- )
76
- }
77
-
78
- // proposed setup for classic E2E tests (node-based, using @nuxt/test-utils)
79
- if (projects.e2e !== false) {
80
- baseConfig.test.projects.push({
81
- extends: true,
82
- test: {
83
- name: 'e2e',
84
- include: ['test/e2e/**/*.{test,spec}.ts'],
85
- environment: 'node',
86
- // create report file for visual regression testing
87
- globalSetup: [screenshotReportSetup],
88
- // allows connecting to an external Playwright instance,
89
- // if NUXT_SPEC_EXTERNAL_PLAYWRIGHT is set
90
- setupFiles: externalPlaywright ? [externalPlaywrightSetup] : [],
91
- },
92
- })
93
- }
94
-
95
- // proposed setup for browser component tests (with Playwright runner)
96
- if (projects.browser !== false) {
97
- // allows connecting to an external Playwright instance,
98
- // if NUXT_SPEC_EXTERNAL_PLAYWRIGHT is set
99
- const playwrightConfig = {}
100
- if (externalPlaywright) {
101
- console.log(`[Nuxt Spec - browser] Using external Playwright instance at: ${externalPlaywright}`)
102
- playwrightConfig.connectOptions = {
103
- wsEndpoint: externalPlaywright,
104
- // this allows reaching caller's localhost from within the external Playwright
105
- exposeNetwork: '<loopback>',
106
- }
107
- }
108
- baseConfig.test.projects.push({
109
- extends: true,
110
- // vue plugin is required for proper imports resolution
111
- plugins: [vue()],
112
- test: {
113
- name: 'browser',
114
- include: ['test/browser/**/*.{test,spec}.ts'],
115
- environment: 'node',
116
- // only chromium browser by default - add others manually if needed
117
- browser: {
118
- provider: playwright(playwrightConfig),
119
- enabled: true,
120
- headless: true,
121
- instances: [{
122
- browser: 'chromium',
123
- viewport: { width: 1280, height: 720 },
124
- }],
125
- },
126
- },
127
- })
128
- }
129
- }
130
-
131
- return mergeConfig(userVitestConfig, defineConfig(baseConfig))
132
- }
1
+ // this is the default Vitest config object
2
+ // based on https://nuxt.com/docs/4.x/getting-started/testing#setup
3
+ // `projects=false` can be used to suspend the default usage of "projects" in Vitest config
4
+
5
+ import { availableParallelism } from 'node:os'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { addMessageFilters, onConsoleLog } from './utils/warnings.mjs' // filter out unnecessary logs
8
+ import { mergeConfig } from './utils/merge.mjs' // defu-based merge function
9
+ import { loadSpecOptions } from './utils/options.mjs' // `spec` key from nuxt.config.ts + NUXT_SPEC_* env variables
10
+ import { nuxtSpecReportPlugin } from './utils/reporter.mjs' // HTML test report
11
+ import { defineConfig } from 'vitest/config'
12
+ import { defineVitestProject } from '@nuxt/test-utils/config'
13
+ import { playwright } from '@vitest/browser-playwright'
14
+ import vue from '@vitejs/plugin-vue'
15
+
16
+ // absolute paths so it works from within the nuxt-ignis package
17
+ const externalPlaywrightSetup = fileURLToPath(new URL('../utils/playwright.ts', import.meta.url))
18
+
19
+ export async function loadVitestConfig(userVitestConfig, projects = true) {
20
+ const { externalPlaywright, htmlReport, messageFilters } = await loadSpecOptions()
21
+
22
+ // extend array of messages that will be filtered out from the output
23
+ // with values passed by the user (if any)
24
+ addMessageFilters(messageFilters)
25
+
26
+ const baseConfig = {
27
+ test: {
28
+ // filter-out unnecessary console logs coming from Vitest
29
+ // when the import is resolved, unnecessary stderr logs are also filtered-out
30
+ // as a side-effect
31
+ onConsoleLog,
32
+ // Vitest defaults to 5 which might be unnecessarily restrictive
33
+ // if the host machine can support more
34
+ maxConcurrency: availableParallelism() / 2,
35
+ // coverage is NOT enabled by default
36
+ // (use `test-c` command or `--coverage` flag)
37
+ // Bun runtime lacks support for the V8 provider
38
+ // (`bun run` without `--bun` still runs Vitest in Node, where V8 works)
39
+ coverage: {
40
+ provider: process.versions.bun ? 'istanbul' : 'v8',
41
+ },
42
+ },
43
+ }
44
+
45
+ // HTML test report for all projects
46
+ // report is generated unless explicitly disabled
47
+ if (htmlReport.enabled) {
48
+ baseConfig.plugins = [nuxtSpecReportPlugin({ open: htmlReport.open })]
49
+ }
50
+
51
+ // add proposed projects settings
52
+ // user can control the inclusion via `projects: ProjectsConfig` config object
53
+ // projects are included by default unless explicitly disabled
54
+ if (projects !== false) {
55
+ baseConfig.test.projects = []
56
+
57
+ // default fallback to catch tests directly in /test folder
58
+ if (projects.default !== false) {
59
+ baseConfig.test.projects.push({
60
+ extends: true,
61
+ test: {
62
+ name: 'default',
63
+ include: ['{test,tests}/**/*.{test,spec}.ts', '!test/{browser,e2e,nuxt,unit}/**'],
64
+ environment: 'node',
65
+ },
66
+ })
67
+ }
68
+
69
+ // proposed setup for Unit tests
70
+ if (projects.unit !== false) {
71
+ baseConfig.test.projects.push({
72
+ extends: true,
73
+ test: {
74
+ name: 'unit',
75
+ include: ['test/unit/**/*.{test,spec}.ts'],
76
+ environment: 'node',
77
+ },
78
+ })
79
+ }
80
+
81
+ // proposed setup for Nuxt component tests
82
+ if (projects.nuxt !== false) {
83
+ baseConfig.test.projects.push(
84
+ await defineVitestProject({
85
+ extends: true,
86
+ test: {
87
+ name: 'nuxt',
88
+ include: ['test/nuxt/**/*.{test,spec}.ts'],
89
+ environment: 'nuxt',
90
+ },
91
+ }),
92
+ )
93
+ }
94
+
95
+ // proposed setup for classic E2E tests (node-based, using @nuxt/test-utils)
96
+ if (projects.e2e !== false) {
97
+ baseConfig.test.projects.push({
98
+ extends: true,
99
+ test: {
100
+ name: 'e2e',
101
+ include: ['test/e2e/**/*.{test,spec}.ts'],
102
+ environment: 'node',
103
+ // allows connecting to an external Playwright instance (if configured)
104
+ setupFiles: externalPlaywright ? [externalPlaywrightSetup] : [],
105
+ // setup file will just read the value via `inject()`
106
+ provide: externalPlaywright ? { nuxtSpecExternalPlaywright: externalPlaywright } : {},
107
+ },
108
+ })
109
+ }
110
+
111
+ // proposed setup for browser component tests (with Playwright runner)
112
+ if (projects.browser !== false) {
113
+ // allows connecting to an external Playwright instance (if configured)
114
+ const playwrightConfig = {}
115
+ if (externalPlaywright) {
116
+ console.log(`[Nuxt Spec] Using external Playwright instance for browser tests at: ${externalPlaywright}`)
117
+ playwrightConfig.connectOptions = {
118
+ wsEndpoint: externalPlaywright,
119
+ // this allows reaching caller's localhost from within the external Playwright
120
+ exposeNetwork: '<loopback>',
121
+ }
122
+ }
123
+ baseConfig.test.projects.push({
124
+ extends: true,
125
+ // vue plugin is required for proper imports resolution
126
+ plugins: [vue()],
127
+ test: {
128
+ name: 'browser',
129
+ include: ['test/browser/**/*.{test,spec}.ts'],
130
+ environment: 'node',
131
+ // only chromium browser by default - add others manually if needed
132
+ browser: {
133
+ provider: playwright(playwrightConfig),
134
+ enabled: true,
135
+ headless: true,
136
+ instances: [{
137
+ browser: 'chromium',
138
+ viewport: { width: 1280, height: 720 },
139
+ }],
140
+ },
141
+ },
142
+ })
143
+ }
144
+ }
145
+
146
+ return mergeConfig(userVitestConfig, defineConfig(baseConfig))
147
+ }
@@ -0,0 +1,6 @@
1
+ <div class="failure">
2
+ <h2>{{TITLE}}</h2>
3
+ <p class="location">{{LOCATION}}</p>
4
+ {{ERRORS}}
5
+ {{SCREENSHOTS}}
6
+ </div>
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Nuxt Spec Test Report - {{TIMESTAMP}}</title>
6
+ <style>
7
+ body { font-family: system-ui, sans-serif; margin: 2rem; background: #f5f5f5; color: #222; }
8
+ h1 { font-size: 1.4rem; }
9
+ .summary { font-weight: 700; font-size: 18px; }
10
+ .summary.passed { color: #0b0; }
11
+ .summary.failed { color: #b00; }
12
+ .failure { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 1rem 0; box-shadow: 0 1px 3px rgba(0, 0, 0, .1); }
13
+ .failure h2 { font-size: 1rem; margin: 0 0 .25rem; }
14
+ .failure .location { color: #666; font-size: .8rem; margin: 0 0 .75rem; }
15
+ .failure pre { white-space: pre-wrap; word-break: break-word; font-size: .8rem; margin: 0 0 .75rem; padding: .5rem; background: #fafafa; border: 1px solid #eee; border-radius: 4px; }
16
+ .failure pre.message { color: #b00; }
17
+ .failure details summary { cursor: pointer; font-size: .8rem; margin-bottom: .25rem; }
18
+ .screenshot { margin-top: .75rem; }
19
+ .screenshot h3 { font-size: .9rem; margin: 0 0 .25rem; }
20
+ .screenshot .message { color: #b00; font-size: .85rem; margin: 0 0 .75rem; }
21
+ .pair { display: flex; gap: 1rem; flex-wrap: wrap; }
22
+ .pair figure { margin: 0; flex: 1 1 0; min-width: 240px; }
23
+ .pair figcaption { font-size: .8rem; margin-bottom: .25rem; font-weight: bold; }
24
+ .pair figure:first-child figcaption { color: #0b0; }
25
+ .pair figure:last-child figcaption { color: #b00; }
26
+ .pair img { max-width: 100%; background: #fff; }
27
+ .pair figure:first-child img { border: 2px solid #0b0; }
28
+ .pair figure:last-child img { border: 2px solid #b00; }
29
+ </style>
30
+ </head>
31
+ <body>
32
+ <h1>Nuxt Spec Test Report - {{TIMESTAMP}}</h1>
33
+ <p class="summary {{STATUS}}">{{SUMMARY}}</p>
34
+ <!-- BEGIN failure entries -->
@@ -1,8 +1,8 @@
1
- <div class="failure">
2
- <h2>{{FILE_NAME}}</h2>
3
- <p class="message">{{MESSAGE}}</p>
4
- <div class="pair">
5
- <figure><figcaption>Baseline image</figcaption><img src="{{BASELINE_URI}}" alt="Baseline image for reference"></figure>
6
- <figure><figcaption>Actual screenshot</figcaption><img src="{{ACTUAL_URI}}" alt="Actual screenshot taken"></figure>
7
- </div>
8
- </div>
1
+ <div class="screenshot">
2
+ <h3>{{FILE_NAME}}</h3>
3
+ <p class="message">{{MESSAGE}}</p>
4
+ <div class="pair">
5
+ <figure><figcaption>Baseline image</figcaption><img src="{{BASELINE_URI}}" alt="Baseline image for reference"></figure>
6
+ <figure><figcaption>Actual screenshot</figcaption><img src="{{ACTUAL_URI}}" alt="Actual screenshot taken"></figure>
7
+ </div>
8
+ </div>
@@ -0,0 +1,4 @@
1
+ <!-- END failure entries -->
2
+ <div>Generated by <a href="https://github.com/AloisSeckar/nuxt-spec">Nuxt Spec</a> @ {{TIMESTAMP}}</div>
3
+ </body>
4
+ </html>
@@ -0,0 +1,105 @@
1
+ # GitHub Actions workflow (scaffolded by Nuxt Spec) to update test baselines (manually triggered).
2
+ # Runs in the same pinned Playwright container as "verify" workflow.
3
+ # Updated baselines (`__baseline__`, `__screenshots__`) and snapshots (`__snapshots__`) are committed back to the dispatched branch.
4
+ # If the push is rejected (e.g. protected branch), a new branch with a pull request is created instead.
5
+ # PREREQUISITE: for the pull request fallback, enable
6
+ # "Settings > Actions > General > Allow GitHub Actions to create and approve pull requests".
7
+ # NOTE: commits and pull requests created via GITHUB_TOKEN don't trigger other workflows - run "verify" manually afterwards.
8
+
9
+ name: Nuxt Spec CI Tests (update baselines)
10
+
11
+ on:
12
+ workflow_dispatch:
13
+
14
+ concurrency:
15
+ group: ci-test-update-${{ github.ref }}
16
+ cancel-in-progress: false
17
+
18
+ permissions:
19
+ contents: write
20
+ pull-requests: write
21
+
22
+ jobs:
23
+ update:
24
+ runs-on: ubuntu-latest
25
+ container:
26
+ # must match `playwright` dependency version
27
+ image: mcr.microsoft.com/playwright:v__PLAYWRIGHT_VERSION__-jammy
28
+ steps:
29
+ - name: Checkout
30
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
31
+ with:
32
+ ref: ${{ github.ref_name }}
33
+
34
+ - name: Enable pnpm via corepack
35
+ run: corepack enable
36
+
37
+ - name: Resolve pnpm store path
38
+ id: pnpm-store
39
+ run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
40
+
41
+ - name: Cache pnpm store
42
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
43
+ with:
44
+ path: ${{ steps.pnpm-store.outputs.path }}
45
+ key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
46
+ restore-keys: pnpm-${{ runner.os }}-
47
+
48
+ - name: Install dependencies
49
+ run: pnpm install --frozen-lockfile
50
+
51
+ - name: Prune pnpm store
52
+ run: pnpm store prune
53
+
54
+ - name: Regenerate test baselines
55
+ run: pnpm test-u
56
+
57
+ - name: Commit updated baselines
58
+ env:
59
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60
+ REPO: ${{ github.repository }}
61
+ REF_NAME: ${{ github.ref_name }}
62
+ RUN_NUMBER: ${{ github.run_number }}
63
+ run: |
64
+ set -eu
65
+
66
+ git config --global --add safe.directory "$GITHUB_WORKSPACE"
67
+ git config user.name "github-actions[bot]"
68
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
69
+
70
+ # `git add` fails on a glob without matches, `git ls-files` doesn't
71
+ git ls-files -z -m -d -o --exclude-standard -- \
72
+ ':(glob)**/__baseline__/**' \
73
+ ':(glob)**/__screenshots__/**' \
74
+ ':(glob)**/__snapshots__/**' \
75
+ | xargs -0 -r git add -A --
76
+ if git diff --cached --quiet; then
77
+ echo "No baseline changes to commit."
78
+ exit 0
79
+ fi
80
+
81
+ git commit -m "test(ci): update test baselines"
82
+
83
+ if git push origin "HEAD:$REF_NAME"; then
84
+ echo "Updated baselines pushed to '$REF_NAME'."
85
+ exit 0
86
+ fi
87
+
88
+ echo "Push to '$REF_NAME' was rejected (e.g. protected branch) - creating pull request..."
89
+ export NEW_BRANCH="ci-test/update-$(date +%Y%m%d%H%M%S)"
90
+ git push origin "HEAD:refs/heads/$NEW_BRANCH"
91
+ BODY=$(node -e '
92
+ const { NEW_BRANCH, REF_NAME, RUN_NUMBER } = process.env
93
+ console.log(JSON.stringify({
94
+ title: "test(ci): update test baselines",
95
+ head: NEW_BRANCH,
96
+ base: REF_NAME,
97
+ body: `Test baselines update triggered on run #${RUN_NUMBER} against branch \`${REF_NAME}\`.`,
98
+ }))')
99
+ curl -sS -X POST \
100
+ -H "Authorization: Bearer $GH_TOKEN" \
101
+ -H "Accept: application/vnd.github+json" \
102
+ -H "Content-Type: application/json" \
103
+ -d "$BODY" \
104
+ --fail-with-body \
105
+ "https://api.github.com/repos/$REPO/pulls"