@remix-run/test 0.0.0 → 0.1.0

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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -2
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +171 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +2 -0
  9. package/dist/lib/config.d.ts +60 -0
  10. package/dist/lib/config.d.ts.map +1 -0
  11. package/dist/lib/config.js +152 -0
  12. package/dist/lib/context.d.ts +69 -0
  13. package/dist/lib/context.d.ts.map +1 -0
  14. package/dist/lib/context.js +49 -0
  15. package/dist/lib/e2e-server.d.ts +11 -0
  16. package/dist/lib/e2e-server.d.ts.map +1 -0
  17. package/dist/lib/e2e-server.js +15 -0
  18. package/dist/lib/executor.d.ts +27 -0
  19. package/dist/lib/executor.d.ts.map +1 -0
  20. package/dist/lib/executor.js +123 -0
  21. package/dist/lib/framework.d.ts +107 -0
  22. package/dist/lib/framework.d.ts.map +1 -0
  23. package/dist/lib/framework.js +198 -0
  24. package/dist/lib/framework.test.d.ts +2 -0
  25. package/dist/lib/framework.test.d.ts.map +1 -0
  26. package/dist/lib/framework.test.e2e.d.ts +2 -0
  27. package/dist/lib/framework.test.e2e.d.ts.map +1 -0
  28. package/dist/lib/framework.test.e2e.js +29 -0
  29. package/dist/lib/framework.test.js +283 -0
  30. package/dist/lib/mock.d.ts +52 -0
  31. package/dist/lib/mock.d.ts.map +1 -0
  32. package/dist/lib/mock.js +61 -0
  33. package/dist/lib/playwright.d.ts +15 -0
  34. package/dist/lib/playwright.d.ts.map +1 -0
  35. package/dist/lib/playwright.js +84 -0
  36. package/dist/lib/reporters/dot.d.ts +10 -0
  37. package/dist/lib/reporters/dot.d.ts.map +1 -0
  38. package/dist/lib/reporters/dot.js +55 -0
  39. package/dist/lib/reporters/files.d.ts +10 -0
  40. package/dist/lib/reporters/files.d.ts.map +1 -0
  41. package/dist/lib/reporters/files.js +70 -0
  42. package/dist/lib/reporters/index.d.ts +14 -0
  43. package/dist/lib/reporters/index.d.ts.map +1 -0
  44. package/dist/lib/reporters/index.js +18 -0
  45. package/dist/lib/reporters/spec.d.ts +10 -0
  46. package/dist/lib/reporters/spec.d.ts.map +1 -0
  47. package/dist/lib/reporters/spec.js +152 -0
  48. package/dist/lib/reporters/tap.d.ts +10 -0
  49. package/dist/lib/reporters/tap.d.ts.map +1 -0
  50. package/dist/lib/reporters/tap.js +54 -0
  51. package/dist/lib/runner.d.ts +9 -0
  52. package/dist/lib/runner.d.ts.map +1 -0
  53. package/dist/lib/runner.js +89 -0
  54. package/dist/lib/utils.d.ts +16 -0
  55. package/dist/lib/utils.d.ts.map +1 -0
  56. package/dist/lib/utils.js +27 -0
  57. package/dist/lib/watcher.d.ts +5 -0
  58. package/dist/lib/watcher.d.ts.map +1 -0
  59. package/dist/lib/watcher.js +39 -0
  60. package/dist/lib/worker-e2e.d.ts +2 -0
  61. package/dist/lib/worker-e2e.d.ts.map +1 -0
  62. package/dist/lib/worker-e2e.js +48 -0
  63. package/dist/lib/worker.d.ts +2 -0
  64. package/dist/lib/worker.d.ts.map +1 -0
  65. package/dist/lib/worker.js +29 -0
  66. package/package.json +58 -5
  67. package/src/cli.ts +210 -0
  68. package/src/index.ts +15 -0
  69. package/src/lib/config.ts +231 -0
  70. package/src/lib/context.ts +126 -0
  71. package/src/lib/e2e-server.ts +28 -0
  72. package/src/lib/executor.ts +162 -0
  73. package/src/lib/framework.ts +251 -0
  74. package/src/lib/mock.ts +89 -0
  75. package/src/lib/playwright.ts +102 -0
  76. package/src/lib/reporters/dot.ts +57 -0
  77. package/src/lib/reporters/files.ts +76 -0
  78. package/src/lib/reporters/index.ts +28 -0
  79. package/src/lib/reporters/spec.ts +173 -0
  80. package/src/lib/reporters/tap.ts +58 -0
  81. package/src/lib/runner.ts +137 -0
  82. package/src/lib/utils.ts +40 -0
  83. package/src/lib/watcher.ts +46 -0
  84. package/src/lib/worker-e2e.ts +52 -0
  85. package/src/lib/worker.ts +30 -0
  86. package/tsconfig.json +14 -0
@@ -0,0 +1,27 @@
1
+ const noColor = process.env.CI === 'true' || !!process.env.NO_COLOR;
2
+ export const colors = {
3
+ reset: noColor ? '' : '\x1b[0m',
4
+ dim: noColor ? (s) => s : (s) => `\x1b[2m${s}\x1b[0m`,
5
+ green: noColor ? (s) => s : (s) => `\x1b[32m${s}\x1b[0m`,
6
+ red: noColor ? (s) => s : (s) => `\x1b[31m${s}\x1b[0m`,
7
+ cyan: noColor ? (s) => s : (s) => `\x1b[36m${s}\x1b[0m`,
8
+ yellow: noColor ? (s) => s : (s) => `\x1b[2m\x1b[33m${s}\x1b[0m`,
9
+ };
10
+ function normalizeFilePath(path) {
11
+ let locSuffix = path.match(/(:\d+:\d+)$/)?.[0] || '';
12
+ let normalized = path
13
+ .replace(/^\/scripts\/@pkg\/([^):]+)/g, (...args) => args[1])
14
+ .replace(/^\/scripts\/@test\/([^):]+)/g, (...args) => args[1])
15
+ .replace(/^\/scripts\/([^):]+)/g, (...args) => args[1])
16
+ .replace(/^\s+/, ' ') + locSuffix;
17
+ return path.includes('/@test/') ? `./${normalized}` : normalized;
18
+ }
19
+ export function normalizeLine(line) {
20
+ let match = line.match(/ \(.*\)$/);
21
+ if (match) {
22
+ let filepath = match[0].slice(2, -1);
23
+ filepath = filepath.replace(/https?:\/\/localhost:\d+\//g, '/');
24
+ return line.slice(0, match.index) + ' (' + normalizeFilePath(filepath) + ')';
25
+ }
26
+ return line;
27
+ }
@@ -0,0 +1,5 @@
1
+ export declare function createWatcher(onChange: (file: string) => void): {
2
+ update: (files: string[]) => void;
3
+ close: () => void;
4
+ };
5
+ //# sourceMappingURL=watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/lib/watcher.ts"],"names":[],"mappings":"AAUA,wBAAgB,aAAa,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;;;EAmC7D"}
@@ -0,0 +1,39 @@
1
+ import * as fs from 'node:fs';
2
+ function getFileModTime(file) {
3
+ try {
4
+ return fs.statSync(file).mtimeMs;
5
+ }
6
+ catch {
7
+ return 0;
8
+ }
9
+ }
10
+ export function createWatcher(onChange) {
11
+ let watchers = new Set();
12
+ let fileModTimes = new Map();
13
+ function update(files) {
14
+ for (let watcher of watchers) {
15
+ watcher.close();
16
+ }
17
+ watchers.clear();
18
+ for (let file of files) {
19
+ fileModTimes.set(file, getFileModTime(file));
20
+ watchers.add(fs.watch(file, () => {
21
+ // macOS FSEvents can fire multiple callbacks per save (e.g. write +
22
+ // metadata flush). Guard with mtime so only a real content change
23
+ // triggers a rerun instead of every duplicate event.
24
+ let mtime = getFileModTime(file);
25
+ if (mtime !== fileModTimes.get(file)) {
26
+ fileModTimes.set(file, mtime);
27
+ onChange(file);
28
+ }
29
+ }));
30
+ }
31
+ }
32
+ function close() {
33
+ for (let watcher of watchers) {
34
+ watcher.close();
35
+ }
36
+ watchers.clear();
37
+ }
38
+ return { update, close };
39
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worker-e2e.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-e2e.d.ts","sourceRoot":"","sources":["../../src/lib/worker-e2e.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ import { workerData, parentPort } from 'node:worker_threads';
2
+ import { tsImport } from 'tsx/esm/api';
3
+ import { createServer } from "./e2e-server.js";
4
+ import { runTests } from "./executor.js";
5
+ import { getBrowserLauncher, getPlaywrightLaunchOptions, getPlaywrightPageOptions, } from "./playwright.js";
6
+ try {
7
+ await tsImport(workerData.file, import.meta.url);
8
+ let launcher = await getBrowserLauncher(workerData.playwrightUseOpts);
9
+ let opts = getPlaywrightLaunchOptions(workerData.playwrightUseOpts);
10
+ let browser = await launcher.launch(opts);
11
+ try {
12
+ let results = await runTests({
13
+ browser,
14
+ createServer,
15
+ open: workerData.open,
16
+ playwrightPageOptions: getPlaywrightPageOptions(workerData.playwrightUseOpts),
17
+ });
18
+ parentPort.postMessage(results);
19
+ if (workerData.open) {
20
+ console.log('\nBrowser is open. Press Ctrl+C to close.');
21
+ await new Promise((resolve) => browser.on('disconnected', () => resolve()));
22
+ }
23
+ }
24
+ finally {
25
+ await browser.close();
26
+ }
27
+ }
28
+ catch (e) {
29
+ let results = {
30
+ passed: 0,
31
+ failed: 1,
32
+ skipped: 0,
33
+ todo: 0,
34
+ tests: [
35
+ {
36
+ name: '',
37
+ suiteName: '',
38
+ status: 'failed',
39
+ duration: 0,
40
+ error: {
41
+ message: e instanceof Error ? e.message : String(e),
42
+ stack: e instanceof Error ? e.stack : undefined,
43
+ },
44
+ },
45
+ ],
46
+ };
47
+ parentPort.postMessage(results);
48
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/lib/worker.ts"],"names":[],"mappings":""}
@@ -0,0 +1,29 @@
1
+ import { workerData, parentPort } from 'node:worker_threads';
2
+ import { tsImport } from 'tsx/esm/api';
3
+ import { runTests } from "./executor.js";
4
+ try {
5
+ await tsImport(workerData.file, import.meta.url);
6
+ let results = await runTests();
7
+ parentPort.postMessage(results);
8
+ }
9
+ catch (e) {
10
+ let results = {
11
+ passed: 0,
12
+ failed: 1,
13
+ skipped: 0,
14
+ todo: 0,
15
+ tests: [
16
+ {
17
+ name: '',
18
+ suiteName: '',
19
+ status: 'failed',
20
+ duration: 0,
21
+ error: {
22
+ message: e instanceof Error ? e.message : String(e),
23
+ stack: e instanceof Error ? e.stack : undefined,
24
+ },
25
+ },
26
+ ],
27
+ };
28
+ parentPort.postMessage(results);
29
+ }
package/package.json CHANGED
@@ -1,14 +1,67 @@
1
1
  {
2
2
  "name": "@remix-run/test",
3
- "version": "0.0.0",
4
- "description": "Placeholder package for Remix CI/OIDC setup",
3
+ "version": "0.1.0",
4
+ "description": "A browser-based test framework for Remix components",
5
+ "author": "Shopify Inc.",
5
6
  "license": "MIT",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/remix-run/remix.git",
9
10
  "directory": "packages/test"
10
11
  },
11
- "publishConfig": {
12
- "access": "public"
12
+ "homepage": "https://github.com/remix-run/remix/tree/main/packages/test#readme",
13
+ "files": [
14
+ "tsconfig.json",
15
+ "LICENSE",
16
+ "README.md",
17
+ "dist",
18
+ "src",
19
+ "!src/**/*.test.*"
20
+ ],
21
+ "type": "module",
22
+ "bin": {
23
+ "remix-test": "./dist/cli.js"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./package.json": "./package.json",
31
+ "./cli": "./dist/cli.js"
32
+ },
33
+ "dependencies": {
34
+ "tsx": "^4.21.0",
35
+ "@remix-run/component": "0.7.0",
36
+ "@remix-run/fetch-router": "0.18.1",
37
+ "@remix-run/node-fetch-server": "0.13.0"
38
+ },
39
+ "peerDependencies": {
40
+ "playwright": "^1.59.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "playwright": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^24.6.0",
49
+ "@typescript/native-preview": "7.0.0-dev.20251125.1",
50
+ "playwright": "^1.59.0",
51
+ "@remix-run/assert": "0.1.0"
52
+ },
53
+ "keywords": [
54
+ "testing",
55
+ "browser",
56
+ "test",
57
+ "remix",
58
+ "component",
59
+ "playwright"
60
+ ],
61
+ "scripts": {
62
+ "build": "tsgo -p tsconfig.build.json",
63
+ "clean": "git clean -fdX",
64
+ "test": "node src/cli.ts",
65
+ "typecheck": "tsgo --noEmit"
13
66
  }
14
- }
67
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ import * as fsp from 'node:fs/promises'
3
+ import * as path from 'node:path'
4
+ import { tsImport } from 'tsx/esm/api'
5
+ import { runServerTests } from './lib/runner.ts'
6
+ import { createReporter } from './lib/reporters/index.ts'
7
+ import { createWatcher } from './lib/watcher.ts'
8
+ import { loadPlaywrightConfig, resolveProjects } from './lib/playwright.ts'
9
+ import { loadConfig, type ResolvedRemixTestConfig } from './lib/config.ts'
10
+ import type { Counts } from './lib/utils.ts'
11
+
12
+ const config = await loadConfig()
13
+
14
+ let hasExited = false
15
+ let latestExitCode = 0
16
+ let watcher: ReturnType<typeof createWatcher> | undefined
17
+ let running = false
18
+ let queued = false
19
+ let rerunTimer: NodeJS.Timeout | undefined
20
+
21
+ process.on('SIGINT', () => cleanupAndExit(latestExitCode))
22
+ process.on('SIGTERM', () => cleanupAndExit(latestExitCode))
23
+
24
+ try {
25
+ await executeRun()
26
+
27
+ if (config.watch) {
28
+ console.log('Watching for changes. Press Ctrl+C to stop.')
29
+ }
30
+ } catch {
31
+ cleanupAndExit(1)
32
+ }
33
+
34
+ async function executeRun() {
35
+ if (hasExited) return
36
+
37
+ running = true
38
+
39
+ let globalTeardown: (() => Promise<void> | void) | undefined
40
+
41
+ try {
42
+ if (config.setup) {
43
+ let mod = await tsImport(path.resolve(process.cwd(), config.setup), {
44
+ parentURL: import.meta.url,
45
+ })
46
+ let globalSetup: (() => Promise<void> | void) | undefined = mod.globalSetup
47
+ globalTeardown = mod.globalTeardown
48
+ await globalSetup?.()
49
+ }
50
+
51
+ let { files, serverFiles, e2eFiles } = await discoverTests(config)
52
+
53
+ if (config.watch) {
54
+ watcher ??= createWatcher((file) => queueRerun(file))
55
+ watcher.update(files)
56
+ }
57
+
58
+ let playwrightConfig =
59
+ config.playwrightConfig == null || typeof config.playwrightConfig === 'string'
60
+ ? await loadPlaywrightConfig(config.playwrightConfig)
61
+ : config.playwrightConfig
62
+
63
+ let reporter = createReporter(config.reporter)
64
+ let startTime = performance.now()
65
+
66
+ let counts: Counts = {
67
+ passed: 0,
68
+ failed: 0,
69
+ skipped: 0,
70
+ todo: 0,
71
+ }
72
+
73
+ // Run server tests
74
+ if (serverFiles.length > 0) {
75
+ reporter.onSectionStart('\nRunning server tests:')
76
+ let serverResult = await runServerTests(serverFiles, reporter, config.concurrency, 'server')
77
+ counts.failed += serverResult.failed
78
+ counts.passed += serverResult.passed
79
+ counts.skipped += serverResult.skipped
80
+ counts.todo += serverResult.todo
81
+ }
82
+
83
+ // Run e2e tests for all browsers configured by the user
84
+ if (e2eFiles.length > 0) {
85
+ let projects = resolveProjects(playwrightConfig)
86
+ if (config.project) {
87
+ let projectNames = config.project.split(',').map((p) => p.trim())
88
+ projects = projects.filter((p) => p.name && projectNames.includes(p.name))
89
+ if (projects.length === 0) {
90
+ throw new Error(`No playwright projects found with name(s) "${config.project}"`)
91
+ }
92
+ }
93
+
94
+ for (let project of projects) {
95
+ reporter.onSectionStart(`\nRunning tests for project \`${project.name}\`:`)
96
+
97
+ if (config.browser?.open) {
98
+ if (project.playwrightUseOpts?.headless === true) {
99
+ let label = project.name ? ` (project "${project.name}")` : ''
100
+ console.warn(
101
+ `Warning: browser.open is set but playwright headless is explicitly true${label} — ignoring browser.open`,
102
+ )
103
+ } else {
104
+ project.playwrightUseOpts = { ...project.playwrightUseOpts, headless: false }
105
+ }
106
+ }
107
+
108
+ let e2eResult =
109
+ e2eFiles.length > 0
110
+ ? await runServerTests(e2eFiles, reporter, config.concurrency, 'e2e', {
111
+ open: config.browser?.open,
112
+ playwrightUseOpts: project.playwrightUseOpts,
113
+ projectName: project.name,
114
+ })
115
+ : null
116
+
117
+ counts.passed += e2eResult?.passed ?? 0
118
+ counts.failed += e2eResult?.failed ?? 0
119
+ counts.skipped += e2eResult?.skipped ?? 0
120
+ counts.todo += e2eResult?.todo ?? 0
121
+ }
122
+ }
123
+
124
+ reporter.onSummary(counts, performance.now() - startTime)
125
+
126
+ latestExitCode = counts.failed > 0 ? 1 : 0
127
+ } catch (error) {
128
+ console.error('Error running tests:', error)
129
+ latestExitCode = 1
130
+ } finally {
131
+ await globalTeardown?.()
132
+ running = false
133
+ if (queued) {
134
+ queued = false
135
+ queueRerun('queued change')
136
+ } else if (!config.watch) {
137
+ cleanupAndExit(latestExitCode)
138
+ }
139
+ }
140
+ }
141
+
142
+ async function discoverTests(config: ResolvedRemixTestConfig): Promise<{
143
+ files: string[]
144
+ serverFiles: string[]
145
+ e2eFiles: string[]
146
+ }> {
147
+ async function findFiles(pattern: string) {
148
+ let files: string[] = []
149
+ let exclude = ['node_modules/**', '.git/**']
150
+
151
+ for await (let file of fsp.glob(pattern, { cwd: process.cwd(), exclude })) {
152
+ files.push(path.resolve(process.cwd(), file))
153
+ }
154
+
155
+ return files
156
+ }
157
+
158
+ let files = await findFiles(config.glob.test)
159
+
160
+ if (files.length === 0) {
161
+ console.log(`No test files found matching pattern: ${config.glob.test}`)
162
+ process.exit(1)
163
+ }
164
+
165
+ let e2eSet = new Set(await findFiles(config.glob.e2e))
166
+
167
+ let types = new Set(config.type.split(','))
168
+ let e2eFiles = types.has('e2e') ? files.filter((f) => e2eSet.has(f)) : []
169
+ let serverFiles = types.has('server') ? files.filter((f) => !e2eSet.has(f)) : []
170
+
171
+ let totalFiles = serverFiles.length + e2eFiles.length
172
+
173
+ if (totalFiles === 0) {
174
+ console.log(`No test files remain after filtering for type ${config.type}`)
175
+ process.exit(1)
176
+ }
177
+
178
+ console.log(
179
+ `Found ${totalFiles} test file(s) (${serverFiles.length} server, ${e2eFiles.length} e2e)`,
180
+ )
181
+
182
+ return {
183
+ files,
184
+ serverFiles,
185
+ e2eFiles,
186
+ }
187
+ }
188
+
189
+ function queueRerun(reason: string) {
190
+ if (!config.watch || hasExited) return
191
+
192
+ clearTimeout(rerunTimer)
193
+
194
+ rerunTimer = setTimeout(() => {
195
+ rerunTimer = undefined
196
+ if (running) {
197
+ queued = true
198
+ } else {
199
+ console.log(`\n↻ Change detected (${reason}), re-running tests...\n`)
200
+ void executeRun()
201
+ }
202
+ }, 100)
203
+ }
204
+
205
+ function cleanupAndExit(code: number) {
206
+ if (hasExited) return
207
+ hasExited = true
208
+ watcher?.close()
209
+ process.exit(code)
210
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export type { RemixTestConfig } from './lib/config.ts'
2
+ export {
3
+ describe,
4
+ it,
5
+ suite,
6
+ test,
7
+ before,
8
+ after,
9
+ beforeEach,
10
+ afterEach,
11
+ beforeAll,
12
+ afterAll,
13
+ } from './lib/framework.ts'
14
+ export { mock } from './lib/mock.ts'
15
+ export type { TestContext } from './lib/context.ts'