dazscript-framework 1.0.29 → 1.0.30
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.
- package/README.md +60 -0
- package/dist/scripts/cli.js +44 -0
- package/dist/scripts/init.js +329 -0
- package/dist/scripts/integration.js +304 -0
- package/dist/scripts/launchers.js +7 -1
- package/package.json +2 -1
- package/src/helpers/script-helper.ts +6 -1
- package/src/scripts/init.test.ts +131 -0
- package/src/scripts/integration.test.ts +97 -0
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
- [Quick Start: A Simple Dialog](#quick-start-a-simple-dialog)
|
|
10
10
|
- [Documentation](#documentation)
|
|
11
11
|
- [Installation & Setup](#installation--setup)
|
|
12
|
+
- [Unit Tests](#unit-tests)
|
|
13
|
+
- [DAZ Studio Integration Tests](#daz-studio-integration-tests)
|
|
12
14
|
- [Project Configuration](#project-configuration)
|
|
13
15
|
- [The `action(...)` Entrypoint](#the-action-entrypoint)
|
|
14
16
|
- [Build Output: Launcher Shims](#build-output-launcher-shims)
|
|
@@ -55,6 +57,13 @@ npx dazscript init
|
|
|
55
57
|
|
|
56
58
|
Follow the prompt for your AppData author namespace (e.g. `YourName/my-project`). This creates `dazscript.config.ts`, `tsconfig.json`, and wires the `build`, `build:encrypted`, `build:release`, `watch`, `encrypt`, `icons`, and `installer` scripts into `package.json`.
|
|
57
59
|
|
|
60
|
+
To include optional test scaffolds, run one or both:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx dazscript init --unit-tests
|
|
64
|
+
npx dazscript init --integration-tests
|
|
65
|
+
```
|
|
66
|
+
|
|
58
67
|
### 3. Write the script
|
|
59
68
|
|
|
60
69
|
Create `src/hello-world.dsa.ts`:
|
|
@@ -166,6 +175,8 @@ Available `init` flags:
|
|
|
166
175
|
|
|
167
176
|
```bash
|
|
168
177
|
npx dazscript init --menu-path /MyScripts --scripts-path ./src --out-dir ./out --app-data-path YourName/my-project
|
|
178
|
+
npx dazscript init --unit-tests --app-data-path YourName/my-project
|
|
179
|
+
npx dazscript init --integration-tests --app-data-path YourName/my-project
|
|
169
180
|
```
|
|
170
181
|
|
|
171
182
|
| Flag | Description |
|
|
@@ -174,11 +185,43 @@ npx dazscript init --menu-path /MyScripts --scripts-path ./src --out-dir ./out -
|
|
|
174
185
|
| `--scripts-path` | Where the generator scans for runnable `.dsa.ts` entry files |
|
|
175
186
|
| `--out-dir` | Where `build` writes `.dsa` files and copies icons |
|
|
176
187
|
| `--app-data-path` | AppData namespace for launcher fallback (`Author/Product` format) |
|
|
188
|
+
| `--unit-tests` | Add Vitest config, sample unit test, unit-test docs, `test` scripts, and the `vitest` dev dependency |
|
|
189
|
+
| `--integration-tests` | Add a DAZ Studio headless integration-test fixture, env examples, ignore entries, and `test:integration` script |
|
|
177
190
|
|
|
178
191
|
Builds also accept `--log-level <trace|debug|info|warn|error|off>`. This sets the minimum runtime log level for the compiled scripts. When omitted, builds default to `debug`; use `warn` for release packages.
|
|
179
192
|
|
|
180
193
|
Use `--scripts-path ./src/scripts` when runnable files live under a subfolder; use `--scripts-path ./src` when they are at the source root.
|
|
181
194
|
|
|
195
|
+
### Unit tests
|
|
196
|
+
|
|
197
|
+
Projects can bootstrap fast Node-side TypeScript tests with:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
npx dazscript init --unit-tests
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
This adds:
|
|
204
|
+
|
|
205
|
+
- `vitest.config.ts`
|
|
206
|
+
- `tsconfig.test.json`
|
|
207
|
+
- `test/unit/smoke.test.ts`
|
|
208
|
+
- `test/unit/README.md`
|
|
209
|
+
- `npm test`
|
|
210
|
+
- `npm run test:watch`
|
|
211
|
+
- `vitest` as a dev dependency
|
|
212
|
+
|
|
213
|
+
Use unit tests for pure TypeScript helpers, parsing, normalization, and code that does not need Daz Studio runtime objects.
|
|
214
|
+
|
|
215
|
+
### DAZ Studio integration tests
|
|
216
|
+
|
|
217
|
+
Projects can run real DAZ Studio smoke tests through the published CLI:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
npm run test:integration
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The generated script calls `dazscript integration --fixture ./test/integration/fixtures/<project>-smoke.dsa.ts`. Configure local machine paths in an ignored `.env.integration.local`; `DAZ_STUDIO_EXE` is always required, and `DAZ_TEST_CONTENT_DUF` is required only for tests that use `--require-content`. See `test/integration/README.md` in this repository for maintainer and consuming-project details.
|
|
224
|
+
|
|
182
225
|
---
|
|
183
226
|
|
|
184
227
|
### Project Configuration
|
|
@@ -528,14 +571,29 @@ my-daz-scripts/
|
|
|
528
571
|
│ ├── my-dialog-model.ts # plain TypeScript — model or helper class
|
|
529
572
|
│ ├── my-dialog.ts
|
|
530
573
|
│ └── my-dialog-script.dsa.ts # runnable entry point
|
|
574
|
+
├── test/
|
|
575
|
+
│ ├── integration/
|
|
576
|
+
│ │ ├── fixtures/
|
|
577
|
+
│ │ │ └── my-daz-scripts-smoke.dsa.ts
|
|
578
|
+
│ │ ├── out/ # integration output, ignored by git
|
|
579
|
+
│ │ └── README.md
|
|
580
|
+
│ └── unit/
|
|
581
|
+
│ ├── smoke.test.ts
|
|
582
|
+
│ └── README.md
|
|
531
583
|
├── out/ # build output — launchers, bundles, icons
|
|
584
|
+
├── .env.integration.linux.example
|
|
585
|
+
├── .env.integration.windows.example
|
|
532
586
|
├── dazscript.config.ts
|
|
533
587
|
├── tsconfig.json
|
|
588
|
+
├── tsconfig.test.json
|
|
589
|
+
├── vitest.config.ts
|
|
534
590
|
└── package.json
|
|
535
591
|
```
|
|
536
592
|
|
|
537
593
|
Files ending in `.dsa.ts` are treated as runnable entry points and compiled to `.dsa`. Plain `.ts` files are modules — imported by entry points but not compiled independently.
|
|
538
594
|
|
|
595
|
+
The `test/unit/` files are generated only when you run `dazscript init --unit-tests`. The `test/integration/` files and env examples are generated only when you run `dazscript init --integration-tests`.
|
|
596
|
+
|
|
539
597
|
**Common commands**
|
|
540
598
|
|
|
541
599
|
| Command | What it does |
|
|
@@ -545,6 +603,8 @@ Files ending in `.dsa.ts` are treated as runnable entry points and compiled to `
|
|
|
545
603
|
| `npm run watch` | Recompile on every save |
|
|
546
604
|
| `npm run installer` | Generate the setup dialog |
|
|
547
605
|
| `npm run icons` | Copy icon assets to the output folder |
|
|
606
|
+
| `npm test` | Run fast Node/Vitest tests |
|
|
607
|
+
| `npm run test:integration` | Run a DAZ Studio headless integration fixture |
|
|
548
608
|
|
|
549
609
|
---
|
|
550
610
|
|
package/dist/scripts/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ const { copyIcons } = require('./icons');
|
|
|
9
9
|
const { runEncrypt } = require('./encrypt');
|
|
10
10
|
const { generateInstallerFiles } = require('./install-generator');
|
|
11
11
|
const { initProject } = require('./init');
|
|
12
|
+
const { runIntegration } = require('./integration');
|
|
12
13
|
|
|
13
14
|
function printHelp() {
|
|
14
15
|
console.log(`dazscript <command> [options]
|
|
@@ -20,6 +21,7 @@ Commands:
|
|
|
20
21
|
encrypt Encrypt built implementation bundles through Daz Studio
|
|
21
22
|
icons Copy png assets into the output directory
|
|
22
23
|
installer Generate Install.dsa.ts and Uninstall.dsa.ts
|
|
24
|
+
integration Run a DAZ Studio headless integration fixture
|
|
23
25
|
|
|
24
26
|
Options:
|
|
25
27
|
--menu-path <path> Default menu path. Default: /MyScripts
|
|
@@ -30,6 +32,11 @@ Options:
|
|
|
30
32
|
--daz-studio <path> Daz Studio executable for encrypt
|
|
31
33
|
--keep-source Keep source script.dsa files after encrypting
|
|
32
34
|
--timeout-ms <value> Daz Studio encrypt timeout. Default: 300000
|
|
35
|
+
--fixture <path> Integration fixture .dsa.ts file
|
|
36
|
+
--env-file <path> Integration env file. Default: .env.integration.local
|
|
37
|
+
--require-content Require DAZ_TEST_CONTENT_DUF for integration tests
|
|
38
|
+
--unit-tests Add Vitest unit-test scaffold during init
|
|
39
|
+
--integration-tests Add integration-test scaffold during init
|
|
33
40
|
--force Overwrite generated files
|
|
34
41
|
--help Show this message
|
|
35
42
|
`);
|
|
@@ -92,6 +99,16 @@ function parseOptions(args, defaults) {
|
|
|
92
99
|
continue;
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
if (arg === '--integration-tests') {
|
|
103
|
+
options.integrationTests = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (arg === '--unit-tests') {
|
|
108
|
+
options.unitTests = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
95
112
|
if (arg === '--menu-path') {
|
|
96
113
|
options.menuPath = args[index + 1];
|
|
97
114
|
index += 1;
|
|
@@ -139,6 +156,23 @@ function parseOptions(args, defaults) {
|
|
|
139
156
|
continue;
|
|
140
157
|
}
|
|
141
158
|
|
|
159
|
+
if (arg === '--fixture') {
|
|
160
|
+
options.fixture = args[index + 1];
|
|
161
|
+
index += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (arg === '--env-file') {
|
|
166
|
+
options.envFile = args[index + 1];
|
|
167
|
+
index += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (arg === '--require-content') {
|
|
172
|
+
options.requireContent = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
142
176
|
if (arg === '--file') {
|
|
143
177
|
options.file = args[index + 1];
|
|
144
178
|
index += 1;
|
|
@@ -191,6 +225,11 @@ async function main(argv) {
|
|
|
191
225
|
dazStudio: undefined,
|
|
192
226
|
keepSource: false,
|
|
193
227
|
timeoutMs: undefined,
|
|
228
|
+
fixture: undefined,
|
|
229
|
+
envFile: undefined,
|
|
230
|
+
requireContent: false,
|
|
231
|
+
unitTests: false,
|
|
232
|
+
integrationTests: false,
|
|
194
233
|
});
|
|
195
234
|
|
|
196
235
|
if (options.help) {
|
|
@@ -198,6 +237,11 @@ async function main(argv) {
|
|
|
198
237
|
return;
|
|
199
238
|
}
|
|
200
239
|
|
|
240
|
+
if (command === 'integration') {
|
|
241
|
+
await runIntegration(options, process.env, workdir);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
201
245
|
const commandOptions =
|
|
202
246
|
command === 'init'
|
|
203
247
|
? await resolveInitOptions(workdir, options)
|
package/dist/scripts/init.js
CHANGED
|
@@ -37,11 +37,28 @@ function writeFileIfNeeded(filePath, content, force) {
|
|
|
37
37
|
return false;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
40
41
|
fs.writeFileSync(filePath, content);
|
|
41
42
|
console.log(`write ${path.basename(filePath)}`);
|
|
42
43
|
return true;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function ensureLine(filePath, line) {
|
|
47
|
+
const existing = fs.existsSync(filePath)
|
|
48
|
+
? fs.readFileSync(filePath, 'utf8')
|
|
49
|
+
: '';
|
|
50
|
+
const lines = existing.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
|
|
51
|
+
|
|
52
|
+
if (lines.includes(line)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
57
|
+
fs.appendFileSync(filePath, `${prefix}${line}\n`);
|
|
58
|
+
console.log(`update ${path.basename(filePath)}`);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
function buildConfigContent(options) {
|
|
46
63
|
return `import { defineConfig } from 'dazscript-framework/config';
|
|
47
64
|
|
|
@@ -63,6 +80,13 @@ function toBundleName(projectName) {
|
|
|
63
80
|
.join(' ');
|
|
64
81
|
}
|
|
65
82
|
|
|
83
|
+
function toFixtureName(projectName) {
|
|
84
|
+
return projectName
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
87
|
+
.replace(/^-+|-+$/g, '') || 'project';
|
|
88
|
+
}
|
|
89
|
+
|
|
66
90
|
function buildTsconfigContent() {
|
|
67
91
|
return `{
|
|
68
92
|
"extends": "./node_modules/dazscript-framework/tsconfig.json",
|
|
@@ -106,6 +130,299 @@ function updatePackageJson(workdir, options) {
|
|
|
106
130
|
console.log('update package.json');
|
|
107
131
|
}
|
|
108
132
|
|
|
133
|
+
function updatePackageJsonForIntegration(workdir, fixturePath) {
|
|
134
|
+
const packageJsonPath = path.join(workdir, 'package.json');
|
|
135
|
+
const packageJson = fs.existsSync(packageJsonPath)
|
|
136
|
+
? readJson(packageJsonPath)
|
|
137
|
+
: { private: true };
|
|
138
|
+
|
|
139
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
140
|
+
if (packageJson.scripts['test:integration']) {
|
|
141
|
+
console.log('skip package.json test:integration');
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
packageJson.scripts['test:integration'] = `dazscript integration --fixture ${fixturePath}`;
|
|
145
|
+
writeJson(packageJsonPath, packageJson);
|
|
146
|
+
console.log('update package.json');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isNpmDefaultTestScript(scriptName, command) {
|
|
151
|
+
return scriptName === 'test' && command === 'echo "Error: no test specified" && exit 1';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function setPackageScript(packageJson, scriptName, command) {
|
|
155
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
156
|
+
const existingCommand = packageJson.scripts[scriptName];
|
|
157
|
+
if (existingCommand && !isNpmDefaultTestScript(scriptName, existingCommand)) {
|
|
158
|
+
console.log(`skip package.json ${scriptName}`);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
packageJson.scripts[scriptName] = command;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function setDevDependency(packageJson, dependencyName, version) {
|
|
167
|
+
packageJson.devDependencies = packageJson.devDependencies || {};
|
|
168
|
+
if (packageJson.devDependencies[dependencyName]) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
packageJson.devDependencies[dependencyName] = version;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function updatePackageJsonForUnitTests(workdir) {
|
|
177
|
+
const packageJsonPath = path.join(workdir, 'package.json');
|
|
178
|
+
const packageJson = fs.existsSync(packageJsonPath)
|
|
179
|
+
? readJson(packageJsonPath)
|
|
180
|
+
: { private: true };
|
|
181
|
+
|
|
182
|
+
let changed = false;
|
|
183
|
+
changed = setPackageScript(packageJson, 'test', 'vitest run') || changed;
|
|
184
|
+
changed = setPackageScript(packageJson, 'test:watch', 'vitest') || changed;
|
|
185
|
+
changed = setDevDependency(packageJson, 'vitest', '^3.0.0') || changed;
|
|
186
|
+
|
|
187
|
+
if (changed) {
|
|
188
|
+
writeJson(packageJsonPath, packageJson);
|
|
189
|
+
console.log('update package.json');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildVitestConfigContent() {
|
|
194
|
+
return `import path from 'node:path'
|
|
195
|
+
import { defineConfig } from 'vitest/config'
|
|
196
|
+
|
|
197
|
+
export default defineConfig({
|
|
198
|
+
test: {
|
|
199
|
+
environment: 'node',
|
|
200
|
+
include: ['src/**/*.test.ts', 'test/unit/**/*.test.ts']
|
|
201
|
+
},
|
|
202
|
+
resolve: {
|
|
203
|
+
alias: {
|
|
204
|
+
'@dsf': path.resolve(__dirname, 'node_modules/dazscript-framework/src'),
|
|
205
|
+
'@dst': path.resolve(__dirname, 'node_modules/dazscript-types/src/types')
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildTestTsconfigContent() {
|
|
213
|
+
return `{
|
|
214
|
+
"extends": "./tsconfig.json",
|
|
215
|
+
"compilerOptions": {
|
|
216
|
+
"moduleResolution": "Node"
|
|
217
|
+
},
|
|
218
|
+
"include": ["src/**/*.test.ts", "test/unit/**/*.test.ts"],
|
|
219
|
+
"exclude": []
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildUnitTestContent() {
|
|
225
|
+
return `import { describe, expect, it } from 'vitest'
|
|
226
|
+
|
|
227
|
+
const toTitleCase = (value: string): string =>
|
|
228
|
+
value
|
|
229
|
+
.split(/[-_\\s]+/)
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
232
|
+
.join(' ')
|
|
233
|
+
|
|
234
|
+
describe('unit test scaffold', () => {
|
|
235
|
+
it('runs TypeScript tests with Vitest', () => {
|
|
236
|
+
expect(toTitleCase('hello-daz-script')).toBe('Hello Daz Script')
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildUnitTestReadmeContent() {
|
|
243
|
+
return `# Unit Tests
|
|
244
|
+
|
|
245
|
+
This project uses Vitest for fast Node-side unit tests.
|
|
246
|
+
|
|
247
|
+
Run once:
|
|
248
|
+
|
|
249
|
+
\`\`\`bash
|
|
250
|
+
npm test
|
|
251
|
+
\`\`\`
|
|
252
|
+
|
|
253
|
+
Watch mode:
|
|
254
|
+
|
|
255
|
+
\`\`\`bash
|
|
256
|
+
npm run test:watch
|
|
257
|
+
\`\`\`
|
|
258
|
+
|
|
259
|
+
Unit tests are useful for pure TypeScript helpers, parsing, normalization, and other logic that does not require Daz Studio runtime objects. Use DAZ Studio integration tests for helpers that must execute inside Daz Studio.
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function initUnitTests(workdir, options) {
|
|
264
|
+
writeFileIfNeeded(
|
|
265
|
+
path.join(workdir, 'vitest.config.ts'),
|
|
266
|
+
buildVitestConfigContent(),
|
|
267
|
+
options.force
|
|
268
|
+
);
|
|
269
|
+
writeFileIfNeeded(
|
|
270
|
+
path.join(workdir, 'tsconfig.test.json'),
|
|
271
|
+
buildTestTsconfigContent(),
|
|
272
|
+
options.force
|
|
273
|
+
);
|
|
274
|
+
writeFileIfNeeded(
|
|
275
|
+
path.join(workdir, 'test/unit/smoke.test.ts'),
|
|
276
|
+
buildUnitTestContent(),
|
|
277
|
+
options.force
|
|
278
|
+
);
|
|
279
|
+
writeFileIfNeeded(
|
|
280
|
+
path.join(workdir, 'test/unit/README.md'),
|
|
281
|
+
buildUnitTestReadmeContent(),
|
|
282
|
+
options.force
|
|
283
|
+
);
|
|
284
|
+
updatePackageJsonForUnitTests(workdir);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildIntegrationFixtureContent() {
|
|
288
|
+
return `import { action } from '@dsf/core/action'
|
|
289
|
+
import { saveToFile } from '@dsf/helpers/file-helper'
|
|
290
|
+
import { currentTime, frameToTime, getCurrentFrame, timeToFrame } from '@dsf/helpers/scene-helper'
|
|
291
|
+
import { getStringScriptArguments } from '@dsf/helpers/script-helper'
|
|
292
|
+
|
|
293
|
+
type IntegrationResult = {
|
|
294
|
+
ok: boolean
|
|
295
|
+
project: string
|
|
296
|
+
checks: Record<string, unknown>
|
|
297
|
+
failures: string[]
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const isNumber = (value: unknown): boolean => typeof value === 'number' && !isNaN(value as number)
|
|
301
|
+
|
|
302
|
+
action({ text: 'Integration Smoke Test', menuPath: false }, () => {
|
|
303
|
+
const args = getStringScriptArguments()
|
|
304
|
+
const resultPath = args.length > 0 ? args[0] : ''
|
|
305
|
+
const failures: string[] = []
|
|
306
|
+
const frame = getCurrentFrame()
|
|
307
|
+
const time = frameToTime(frame)
|
|
308
|
+
const current = currentTime()
|
|
309
|
+
const roundTripFrame = timeToFrame(time)
|
|
310
|
+
|
|
311
|
+
if (!resultPath) {
|
|
312
|
+
failures.push('missing result path argument')
|
|
313
|
+
}
|
|
314
|
+
if (!isNumber(frame)) {
|
|
315
|
+
failures.push('scene-helper getCurrentFrame did not return a number')
|
|
316
|
+
}
|
|
317
|
+
if (current === null || current === undefined) {
|
|
318
|
+
failures.push('scene-helper currentTime returned null or undefined')
|
|
319
|
+
}
|
|
320
|
+
if (roundTripFrame !== frame) {
|
|
321
|
+
failures.push(\`frame/time round trip failed: \${frame} -> \${time} -> \${roundTripFrame}\`)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const result: IntegrationResult = {
|
|
325
|
+
ok: failures.length === 0,
|
|
326
|
+
project: 'integration-smoke',
|
|
327
|
+
checks: {
|
|
328
|
+
frame,
|
|
329
|
+
time: String(time),
|
|
330
|
+
currentTime: String(current),
|
|
331
|
+
roundTripFrame
|
|
332
|
+
},
|
|
333
|
+
failures
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (resultPath) {
|
|
337
|
+
saveToFile(resultPath, JSON.stringify(result, null, 2))
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function buildIntegrationReadmeContent(fixturePath) {
|
|
344
|
+
return `# Integration Tests
|
|
345
|
+
|
|
346
|
+
This project uses the framework DAZ Studio headless integration harness.
|
|
347
|
+
|
|
348
|
+
Create a local env file from the example for your OS:
|
|
349
|
+
|
|
350
|
+
\`\`\`bash
|
|
351
|
+
cp .env.integration.linux.example .env.integration.local
|
|
352
|
+
\`\`\`
|
|
353
|
+
|
|
354
|
+
\`\`\`powershell
|
|
355
|
+
Copy-Item .env.integration.windows.example .env.integration.local
|
|
356
|
+
\`\`\`
|
|
357
|
+
|
|
358
|
+
Then run:
|
|
359
|
+
|
|
360
|
+
\`\`\`bash
|
|
361
|
+
npm run test:integration
|
|
362
|
+
\`\`\`
|
|
363
|
+
|
|
364
|
+
The default smoke fixture is figure-independent:
|
|
365
|
+
|
|
366
|
+
\`\`\`text
|
|
367
|
+
${fixturePath}
|
|
368
|
+
\`\`\`
|
|
369
|
+
|
|
370
|
+
Generated output is written to \`test/integration/out/\` and ignored by git.
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function buildLinuxEnvExample() {
|
|
375
|
+
return `# Copy this file to .env.integration.local and edit paths for your machine.
|
|
376
|
+
# Shell environment variables override values in .env.integration.local.
|
|
377
|
+
# DAZ_STUDIO_EXE is required for all integration tests.
|
|
378
|
+
# DAZ_TEST_CONTENT_DUF is required only when the fixture or npm script uses --require-content.
|
|
379
|
+
|
|
380
|
+
WINEPREFIX=/home/your-user/.local/share/daz-wine/prefix
|
|
381
|
+
DAZ_STUDIO_EXE=/home/your-user/.local/share/daz-wine/prefix/drive_c/Program Files/DAZ 3D/DAZStudio4/DAZStudio.exe
|
|
382
|
+
# DAZ_TEST_CONTENT_DUF=/home/your-user/.local/share/daz-wine/prefix/drive_c/users/Public/Documents/My DAZ 3D Library/People/Genesis 9/Genesis 9.duf
|
|
383
|
+
DAZ_TEST_TIMEOUT_MS=300000
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function buildWindowsEnvExample() {
|
|
388
|
+
return `# Copy this file to .env.integration.local and edit paths for your machine.
|
|
389
|
+
# Shell environment variables override values in .env.integration.local.
|
|
390
|
+
# Forward slashes avoid escaping issues in env files.
|
|
391
|
+
# DAZ_STUDIO_EXE is required for all integration tests.
|
|
392
|
+
# DAZ_TEST_CONTENT_DUF is required only when the fixture or npm script uses --require-content.
|
|
393
|
+
|
|
394
|
+
DAZ_STUDIO_EXE=C:/Program Files/DAZ 3D/DAZStudio4/DAZStudio.exe
|
|
395
|
+
# DAZ_TEST_CONTENT_DUF=C:/Users/Public/Documents/My DAZ 3D Library/People/Genesis 9/Genesis 9.duf
|
|
396
|
+
DAZ_TEST_TIMEOUT_MS=300000
|
|
397
|
+
`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function initIntegrationTests(workdir, options) {
|
|
401
|
+
const fixtureBaseName = `${toFixtureName(path.basename(workdir))}-smoke.dsa.ts`;
|
|
402
|
+
const fixturePath = `./test/integration/fixtures/${fixtureBaseName}`;
|
|
403
|
+
const localFixturePath = path.join(workdir, fixturePath);
|
|
404
|
+
|
|
405
|
+
writeFileIfNeeded(localFixturePath, buildIntegrationFixtureContent(), options.force);
|
|
406
|
+
writeFileIfNeeded(
|
|
407
|
+
path.join(workdir, 'test/integration/README.md'),
|
|
408
|
+
buildIntegrationReadmeContent(fixturePath),
|
|
409
|
+
options.force
|
|
410
|
+
);
|
|
411
|
+
writeFileIfNeeded(
|
|
412
|
+
path.join(workdir, '.env.integration.linux.example'),
|
|
413
|
+
buildLinuxEnvExample(),
|
|
414
|
+
options.force
|
|
415
|
+
);
|
|
416
|
+
writeFileIfNeeded(
|
|
417
|
+
path.join(workdir, '.env.integration.windows.example'),
|
|
418
|
+
buildWindowsEnvExample(),
|
|
419
|
+
options.force
|
|
420
|
+
);
|
|
421
|
+
updatePackageJsonForIntegration(workdir, fixturePath);
|
|
422
|
+
ensureLine(path.join(workdir, '.gitignore'), 'test/integration/out/');
|
|
423
|
+
ensureLine(path.join(workdir, '.gitignore'), '.env.integration.local');
|
|
424
|
+
}
|
|
425
|
+
|
|
109
426
|
function initProject(workdir, rawOptions) {
|
|
110
427
|
const projectName = path.basename(workdir);
|
|
111
428
|
const options = {
|
|
@@ -115,6 +432,8 @@ function initProject(workdir, rawOptions) {
|
|
|
115
432
|
outDir: normalizePath(rawOptions.outDir, './out'),
|
|
116
433
|
appDataPath: normalizePath(rawOptions.appDataPath, `YourName/${projectName}`),
|
|
117
434
|
bundleName: toBundleName(projectName),
|
|
435
|
+
unitTests: Boolean(rawOptions.unitTests),
|
|
436
|
+
integrationTests: Boolean(rawOptions.integrationTests),
|
|
118
437
|
};
|
|
119
438
|
|
|
120
439
|
writeFileIfNeeded(
|
|
@@ -129,8 +448,18 @@ function initProject(workdir, rawOptions) {
|
|
|
129
448
|
);
|
|
130
449
|
|
|
131
450
|
updatePackageJson(workdir, options);
|
|
451
|
+
|
|
452
|
+
if (options.unitTests) {
|
|
453
|
+
initUnitTests(workdir, options);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (options.integrationTests) {
|
|
457
|
+
initIntegrationTests(workdir, options);
|
|
458
|
+
}
|
|
132
459
|
}
|
|
133
460
|
|
|
134
461
|
module.exports = {
|
|
462
|
+
initIntegrationTests,
|
|
463
|
+
initUnitTests,
|
|
135
464
|
initProject,
|
|
136
465
|
};
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const defaultTimeoutMs = 300000;
|
|
8
|
+
|
|
9
|
+
function isDazPath(value) {
|
|
10
|
+
return /^[A-Za-z]:[\\/]/.test(String(value || ''));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeForDaz(value) {
|
|
14
|
+
const normalized = String(value).replace(/\\/g, '/');
|
|
15
|
+
const driveMatch = normalized.match(/\/drive_([A-Za-z])\/(.+)$/);
|
|
16
|
+
if (driveMatch) {
|
|
17
|
+
return `${driveMatch[1].toUpperCase()}:/${driveMatch[2]}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unquoteEnvValue(value) {
|
|
24
|
+
const trimmed = String(value || '').trim();
|
|
25
|
+
if (trimmed.length < 2) return trimmed;
|
|
26
|
+
|
|
27
|
+
const first = trimmed[0];
|
|
28
|
+
const last = trimmed[trimmed.length - 1];
|
|
29
|
+
if ((first === '"' && last === '"') || (first === '\'' && last === '\'')) {
|
|
30
|
+
return trimmed.substring(1, trimmed.length - 1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return trimmed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadEnvFile(envPath, targetEnv) {
|
|
37
|
+
if (!envPath || !fs.existsSync(envPath)) return false;
|
|
38
|
+
|
|
39
|
+
const lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/);
|
|
40
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
41
|
+
const line = lines[index].trim();
|
|
42
|
+
if (!line || line[0] === '#') continue;
|
|
43
|
+
|
|
44
|
+
const separatorIndex = line.indexOf('=');
|
|
45
|
+
if (separatorIndex <= 0) continue;
|
|
46
|
+
|
|
47
|
+
const key = line.substring(0, separatorIndex).trim();
|
|
48
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
49
|
+
if (targetEnv[key] !== undefined) continue;
|
|
50
|
+
|
|
51
|
+
targetEnv[key] = unquoteEnvValue(line.substring(separatorIndex + 1));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function assertRequiredFile(label, filePath, allowDazPath) {
|
|
58
|
+
if (!filePath) {
|
|
59
|
+
throw new Error(`missing required env: ${label}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (allowDazPath && isDazPath(filePath)) {
|
|
63
|
+
return filePath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const resolvedPath = path.resolve(filePath);
|
|
67
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
68
|
+
throw new Error(`${label} does not exist: ${resolvedPath}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return resolvedPath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveIntegrationOptions(cwd, rawOptions, env) {
|
|
75
|
+
const options = rawOptions || {};
|
|
76
|
+
const projectRoot = path.resolve(cwd || process.cwd());
|
|
77
|
+
const envFile = path.resolve(projectRoot, options.envFile || '.env.integration.local');
|
|
78
|
+
const fixturePath = options.fixture
|
|
79
|
+
? path.resolve(projectRoot, options.fixture)
|
|
80
|
+
: '';
|
|
81
|
+
|
|
82
|
+
if (!fixturePath) {
|
|
83
|
+
throw new Error('missing required option: --fixture <path>');
|
|
84
|
+
}
|
|
85
|
+
if (!fs.existsSync(fixturePath)) {
|
|
86
|
+
throw new Error(`fixture file does not exist: ${fixturePath}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const dazStudioExe = assertRequiredFile(
|
|
90
|
+
'DAZ_STUDIO_EXE',
|
|
91
|
+
options.dazStudio || env.DAZ_STUDIO_EXE,
|
|
92
|
+
false
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const rawContentPath = options.contentDuf || env.DAZ_TEST_CONTENT_DUF || '';
|
|
96
|
+
if (options.requireContent) {
|
|
97
|
+
assertRequiredFile('DAZ_TEST_CONTENT_DUF', rawContentPath, true);
|
|
98
|
+
}
|
|
99
|
+
else if (rawContentPath && !isDazPath(rawContentPath) && !fs.existsSync(path.resolve(rawContentPath))) {
|
|
100
|
+
throw new Error(`DAZ_TEST_CONTENT_DUF does not exist: ${path.resolve(rawContentPath)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const timeoutMs = Number(options.timeoutMs || env.DAZ_TEST_TIMEOUT_MS || defaultTimeoutMs);
|
|
104
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
105
|
+
throw new Error(`invalid timeout: ${options.timeoutMs || env.DAZ_TEST_TIMEOUT_MS}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const outDir = path.resolve(projectRoot, options.outDir || './test/integration/out');
|
|
109
|
+
const fixtureName = path.basename(fixturePath).replace(/\.dsa\.ts$/i, '').replace(/\.ts$/i, '');
|
|
110
|
+
const fixtureRoot = path.join(outDir, 'fixture');
|
|
111
|
+
const resultPath = path.join(fixtureRoot, 'result.json');
|
|
112
|
+
const frameworkRoot = path.resolve(__dirname, '..', '..');
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
projectRoot,
|
|
116
|
+
frameworkRoot,
|
|
117
|
+
envFile,
|
|
118
|
+
fixturePath,
|
|
119
|
+
fixtureName,
|
|
120
|
+
outDir,
|
|
121
|
+
fixtureRoot,
|
|
122
|
+
resultPath,
|
|
123
|
+
dazStudioExe,
|
|
124
|
+
contentPath: rawContentPath ? normalizeForDaz(rawContentPath) : '',
|
|
125
|
+
requireContent: Boolean(options.requireContent),
|
|
126
|
+
timeoutMs,
|
|
127
|
+
appDataPath: options.appDataPath || 'DazScriptFramework/integration-tests',
|
|
128
|
+
bundleName: options.bundleName || 'Integration Test Fixture',
|
|
129
|
+
env,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function run(command, args, options) {
|
|
134
|
+
const result = spawnSync(command, args, {
|
|
135
|
+
cwd: options.cwd,
|
|
136
|
+
env: options.env || process.env,
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
stdio: options.stdio || 'inherit',
|
|
139
|
+
timeout: options.timeoutMs,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (result.error) {
|
|
143
|
+
if (result.error.code === 'ETIMEDOUT') {
|
|
144
|
+
throw new Error(`${command} timed out after ${options.timeoutMs}ms`);
|
|
145
|
+
}
|
|
146
|
+
throw result.error;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (result.status !== 0) {
|
|
150
|
+
throw new Error(`${command} ${args.join(' ')} exited with code ${result.status}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function writeFile(filePath, content) {
|
|
157
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
158
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildFixtureProject(options) {
|
|
162
|
+
fs.rmSync(options.fixtureRoot, { recursive: true, force: true });
|
|
163
|
+
fs.mkdirSync(path.join(options.fixtureRoot, 'src'), { recursive: true });
|
|
164
|
+
|
|
165
|
+
writeFile(path.join(options.fixtureRoot, 'package.json'), JSON.stringify({
|
|
166
|
+
private: true,
|
|
167
|
+
scripts: {
|
|
168
|
+
build: 'dazscript build --out-dir ./out',
|
|
169
|
+
},
|
|
170
|
+
dependencies: {
|
|
171
|
+
'dazscript-framework': `file:${options.frameworkRoot}`,
|
|
172
|
+
'dazscript-types': '^1.0.1',
|
|
173
|
+
},
|
|
174
|
+
devDependencies: {},
|
|
175
|
+
}, null, 2));
|
|
176
|
+
|
|
177
|
+
writeFile(path.join(options.fixtureRoot, 'dazscript.config.ts'), [
|
|
178
|
+
"import { defineConfig } from 'dazscript-framework/config';",
|
|
179
|
+
'',
|
|
180
|
+
'export default defineConfig({',
|
|
181
|
+
" scriptsPath: './src',",
|
|
182
|
+
" outDir: './out',",
|
|
183
|
+
" defaultMenuPath: '/DazScriptFramework/Integration',",
|
|
184
|
+
` appDataPath: '${options.appDataPath}',`,
|
|
185
|
+
` bundleName: '${options.bundleName}'`,
|
|
186
|
+
'});',
|
|
187
|
+
'',
|
|
188
|
+
].join('\n'));
|
|
189
|
+
|
|
190
|
+
writeFile(path.join(options.fixtureRoot, 'tsconfig.json'), JSON.stringify({
|
|
191
|
+
compilerOptions: {
|
|
192
|
+
target: 'ES5',
|
|
193
|
+
baseUrl: '.',
|
|
194
|
+
ignoreDeprecations: '5.0',
|
|
195
|
+
lib: ['ES5'],
|
|
196
|
+
experimentalDecorators: true,
|
|
197
|
+
emitDecoratorMetadata: true,
|
|
198
|
+
noLib: false,
|
|
199
|
+
useDefineForClassFields: false,
|
|
200
|
+
module: 'ESNext',
|
|
201
|
+
paths: {
|
|
202
|
+
'@dst/*': ['node_modules/dazscript-types/src/types/*'],
|
|
203
|
+
'@dsf/*': ['node_modules/dazscript-framework/src/*'],
|
|
204
|
+
},
|
|
205
|
+
declaration: false,
|
|
206
|
+
inlineSourceMap: false,
|
|
207
|
+
removeComments: true,
|
|
208
|
+
preserveConstEnums: true,
|
|
209
|
+
esModuleInterop: true,
|
|
210
|
+
forceConsistentCasingInFileNames: true,
|
|
211
|
+
strict: false,
|
|
212
|
+
strictNullChecks: false,
|
|
213
|
+
strictPropertyInitialization: false,
|
|
214
|
+
noImplicitAny: false,
|
|
215
|
+
strictFunctionTypes: true,
|
|
216
|
+
alwaysStrict: true,
|
|
217
|
+
skipLibCheck: true,
|
|
218
|
+
},
|
|
219
|
+
include: [
|
|
220
|
+
'node_modules/dazscript-types/src/types/**/*',
|
|
221
|
+
'src/**/*',
|
|
222
|
+
],
|
|
223
|
+
}, null, 2));
|
|
224
|
+
|
|
225
|
+
fs.copyFileSync(options.fixturePath, path.join(options.fixtureRoot, 'src', `${options.fixtureName}.dsa.ts`));
|
|
226
|
+
run('npm', ['install', '--ignore-scripts'], { cwd: options.fixtureRoot });
|
|
227
|
+
run('npm', ['run', 'build'], { cwd: options.fixtureRoot });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function runDazFixture(options) {
|
|
231
|
+
const launcherPath = path.join(options.fixtureRoot, 'out', `${options.fixtureName}.dsa`);
|
|
232
|
+
if (!fs.existsSync(launcherPath)) {
|
|
233
|
+
throw new Error(`generated launcher was not found: ${launcherPath}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const dazArgs = [
|
|
237
|
+
'-headless',
|
|
238
|
+
'-noPrompt',
|
|
239
|
+
'-scriptArg',
|
|
240
|
+
normalizeForDaz(options.resultPath),
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
if (options.contentPath) {
|
|
244
|
+
dazArgs.push('-scriptArg', options.contentPath);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
dazArgs.push('-script', normalizeForDaz(launcherPath));
|
|
248
|
+
|
|
249
|
+
const useWine = process.platform !== 'win32' && /\.exe$/i.test(options.dazStudioExe);
|
|
250
|
+
const command = useWine ? 'wine' : options.dazStudioExe;
|
|
251
|
+
const commandArgs = useWine ? [options.dazStudioExe].concat(dazArgs) : dazArgs;
|
|
252
|
+
|
|
253
|
+
console.log(`[dazscript integration] launching DAZ: ${useWine ? `${command} ${options.dazStudioExe}` : command}`);
|
|
254
|
+
run(command, commandArgs, {
|
|
255
|
+
cwd: options.fixtureRoot,
|
|
256
|
+
env: options.env,
|
|
257
|
+
timeoutMs: options.timeoutMs,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function readIntegrationResult(resultPath) {
|
|
262
|
+
if (!fs.existsSync(resultPath)) {
|
|
263
|
+
throw new Error(`DAZ did not write result JSON: ${resultPath}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let result;
|
|
267
|
+
try {
|
|
268
|
+
result = JSON.parse(fs.readFileSync(resultPath, 'utf8'));
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
throw new Error(`could not parse result JSON: ${error}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!result || result.ok !== true) {
|
|
275
|
+
const failures = result && result.failures ? result.failures : ['unknown DAZ integration failure'];
|
|
276
|
+
throw new Error(`DAZ integration assertions failed:\n${failures.map((item) => `- ${item}`).join('\n')}\n\nResult: ${resultPath}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function runIntegration(rawOptions, injectedEnv, cwd) {
|
|
283
|
+
const env = { ...(injectedEnv || process.env) };
|
|
284
|
+
const projectRoot = path.resolve(cwd || process.cwd());
|
|
285
|
+
const envFile = path.resolve(projectRoot, (rawOptions && rawOptions.envFile) || '.env.integration.local');
|
|
286
|
+
loadEnvFile(envFile, env);
|
|
287
|
+
|
|
288
|
+
const options = resolveIntegrationOptions(projectRoot, rawOptions, env);
|
|
289
|
+
buildFixtureProject(options);
|
|
290
|
+
runDazFixture(options);
|
|
291
|
+
const result = readIntegrationResult(options.resultPath);
|
|
292
|
+
console.log(`[dazscript integration] passed: ${options.resultPath}`);
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
loadEnvFile,
|
|
298
|
+
normalizeForDaz,
|
|
299
|
+
readIntegrationResult,
|
|
300
|
+
resolveIntegrationOptions,
|
|
301
|
+
buildFixtureProject,
|
|
302
|
+
runDazFixture,
|
|
303
|
+
runIntegration,
|
|
304
|
+
};
|
|
@@ -96,7 +96,13 @@ function makeLauncherSource(implementationRelativePath, appDataImplementationRel
|
|
|
96
96
|
` MessageBox.warning('Unable to load script implementation:\\n' + implementationPath, 'Script Load Error', '&OK;', '');`,
|
|
97
97
|
' script.deleteLater();',
|
|
98
98
|
' } else {',
|
|
99
|
-
|
|
99
|
+
' var args = [];',
|
|
100
|
+
" if (typeof getArguments == 'function') {",
|
|
101
|
+
' args = getArguments();',
|
|
102
|
+
' }',
|
|
103
|
+
' if (!args || args.length === 0) {',
|
|
104
|
+
' args = App.scriptArgs || [];',
|
|
105
|
+
' }',
|
|
100
106
|
' script.execute(args);',
|
|
101
107
|
' script.deleteLater();',
|
|
102
108
|
' }',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dazscript-framework",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30",
|
|
4
4
|
"author": "Freddy Diaz",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"description": "",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"icons": "node ./dist/scripts/cli.js icons --out-dir ./out",
|
|
14
14
|
"installer": "node ./dist/scripts/cli.js installer --scripts-path ./src/examples --menu-path /DazScriptFramework",
|
|
15
15
|
"test": "vitest run",
|
|
16
|
+
"test:integration": "node ./dist/scripts/cli.js integration --fixture ./test/integration/fixtures/framework-integration.dsa.ts --require-content",
|
|
16
17
|
"test:watch": "vitest"
|
|
17
18
|
},
|
|
18
19
|
"bin": {
|
|
@@ -17,7 +17,12 @@ export const getScriptPath = (): string => {
|
|
|
17
17
|
|
|
18
18
|
export const getScriptArguments = (): any[] => {
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
if (typeof getArguments === 'function') {
|
|
21
|
+
const args = getArguments()
|
|
22
|
+
if (args && args.length > 0) return args
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return App.scriptArgs ?? []
|
|
21
26
|
}
|
|
22
27
|
catch (e) {
|
|
23
28
|
return []
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
const { initIntegrationTests, initUnitTests } = require('../../dist/scripts/init')
|
|
7
|
+
|
|
8
|
+
const tempDirs: string[] = []
|
|
9
|
+
|
|
10
|
+
const makeProject = (name = 'toolbox-test'): string => {
|
|
11
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), `${name}-`))
|
|
12
|
+
tempDirs.push(projectDir)
|
|
13
|
+
fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify({
|
|
14
|
+
private: true,
|
|
15
|
+
scripts: {
|
|
16
|
+
test: 'vitest run',
|
|
17
|
+
},
|
|
18
|
+
}, null, 2))
|
|
19
|
+
return projectDir
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const toFixtureName = (projectDir: string): string =>
|
|
23
|
+
path.basename(projectDir)
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
26
|
+
.replace(/^-+|-+$/g, '') || 'project'
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
while (tempDirs.length > 0) {
|
|
30
|
+
const dir = tempDirs.pop()
|
|
31
|
+
if (dir) fs.rmSync(dir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('init integration tests', () => {
|
|
36
|
+
it('creates the fixture, docs, env examples, npm script, and ignore entries', () => {
|
|
37
|
+
const projectDir = makeProject('toolbox-test')
|
|
38
|
+
const fixtureBaseName = toFixtureName(projectDir)
|
|
39
|
+
|
|
40
|
+
initIntegrationTests(projectDir, { force: false })
|
|
41
|
+
|
|
42
|
+
const fixturePath = path.join(projectDir, `test/integration/fixtures/${fixtureBaseName}-smoke.dsa.ts`)
|
|
43
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8'))
|
|
44
|
+
const gitignore = fs.readFileSync(path.join(projectDir, '.gitignore'), 'utf8')
|
|
45
|
+
|
|
46
|
+
expect(fs.existsSync(fixturePath)).toBe(true)
|
|
47
|
+
expect(fs.existsSync(path.join(projectDir, 'test/integration/README.md'))).toBe(true)
|
|
48
|
+
expect(fs.existsSync(path.join(projectDir, '.env.integration.linux.example'))).toBe(true)
|
|
49
|
+
expect(fs.existsSync(path.join(projectDir, '.env.integration.windows.example'))).toBe(true)
|
|
50
|
+
expect(packageJson.scripts['test:integration']).toBe(
|
|
51
|
+
`dazscript integration --fixture ./test/integration/fixtures/${fixtureBaseName}-smoke.dsa.ts`
|
|
52
|
+
)
|
|
53
|
+
expect(gitignore).toContain('test/integration/out/')
|
|
54
|
+
expect(gitignore).toContain('.env.integration.local')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('does not replace an existing integration script or duplicate ignore entries', () => {
|
|
58
|
+
const projectDir = makeProject('existing-test')
|
|
59
|
+
const packageJsonPath = path.join(projectDir, 'package.json')
|
|
60
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
61
|
+
packageJson.scripts['test:integration'] = 'custom command'
|
|
62
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
|
|
63
|
+
fs.writeFileSync(path.join(projectDir, '.gitignore'), 'test/integration/out/\n')
|
|
64
|
+
|
|
65
|
+
initIntegrationTests(projectDir, { force: false })
|
|
66
|
+
initIntegrationTests(projectDir, { force: false })
|
|
67
|
+
|
|
68
|
+
const updatedPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
69
|
+
const gitignore = fs.readFileSync(path.join(projectDir, '.gitignore'), 'utf8')
|
|
70
|
+
|
|
71
|
+
expect(updatedPackageJson.scripts['test:integration']).toBe('custom command')
|
|
72
|
+
expect(gitignore.match(/test\/integration\/out\//g)?.length).toBe(1)
|
|
73
|
+
expect(gitignore.match(/\.env\.integration\.local/g)?.length).toBe(1)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('init unit tests', () => {
|
|
78
|
+
it('creates Vitest config, sample test, docs, npm scripts, and dev dependency', () => {
|
|
79
|
+
const projectDir = makeProject('unit-test')
|
|
80
|
+
const packageJsonPath = path.join(projectDir, 'package.json')
|
|
81
|
+
const initialPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
82
|
+
delete initialPackageJson.scripts.test
|
|
83
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(initialPackageJson, null, 2))
|
|
84
|
+
|
|
85
|
+
initUnitTests(projectDir, { force: false })
|
|
86
|
+
|
|
87
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8'))
|
|
88
|
+
|
|
89
|
+
expect(fs.existsSync(path.join(projectDir, 'vitest.config.ts'))).toBe(true)
|
|
90
|
+
expect(fs.existsSync(path.join(projectDir, 'tsconfig.test.json'))).toBe(true)
|
|
91
|
+
expect(fs.existsSync(path.join(projectDir, 'test/unit/smoke.test.ts'))).toBe(true)
|
|
92
|
+
expect(fs.existsSync(path.join(projectDir, 'test/unit/README.md'))).toBe(true)
|
|
93
|
+
expect(packageJson.scripts.test).toBe('vitest run')
|
|
94
|
+
expect(packageJson.scripts['test:watch']).toBe('vitest')
|
|
95
|
+
expect(packageJson.devDependencies.vitest).toBe('^3.0.0')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('does not replace existing unit test scripts or duplicate Vitest dependency', () => {
|
|
99
|
+
const projectDir = makeProject('existing-unit-test')
|
|
100
|
+
const packageJsonPath = path.join(projectDir, 'package.json')
|
|
101
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
102
|
+
packageJson.scripts.test = 'custom test'
|
|
103
|
+
packageJson.scripts['test:watch'] = 'custom watch'
|
|
104
|
+
packageJson.devDependencies = {
|
|
105
|
+
vitest: '^4.0.0',
|
|
106
|
+
}
|
|
107
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
|
|
108
|
+
|
|
109
|
+
initUnitTests(projectDir, { force: false })
|
|
110
|
+
|
|
111
|
+
const updatedPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
112
|
+
|
|
113
|
+
expect(updatedPackageJson.scripts.test).toBe('custom test')
|
|
114
|
+
expect(updatedPackageJson.scripts['test:watch']).toBe('custom watch')
|
|
115
|
+
expect(updatedPackageJson.devDependencies.vitest).toBe('^4.0.0')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('replaces the default npm init failing test script', () => {
|
|
119
|
+
const projectDir = makeProject('npm-default-unit-test')
|
|
120
|
+
const packageJsonPath = path.join(projectDir, 'package.json')
|
|
121
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
122
|
+
packageJson.scripts.test = 'echo "Error: no test specified" && exit 1'
|
|
123
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
|
|
124
|
+
|
|
125
|
+
initUnitTests(projectDir, { force: false })
|
|
126
|
+
|
|
127
|
+
const updatedPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
128
|
+
|
|
129
|
+
expect(updatedPackageJson.scripts.test).toBe('vitest run')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
loadEnvFile,
|
|
8
|
+
readIntegrationResult,
|
|
9
|
+
resolveIntegrationOptions,
|
|
10
|
+
} = require('../../dist/scripts/integration')
|
|
11
|
+
|
|
12
|
+
const tempDirs: string[] = []
|
|
13
|
+
|
|
14
|
+
const makeProject = (): string => {
|
|
15
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsf-integration-'))
|
|
16
|
+
tempDirs.push(projectDir)
|
|
17
|
+
fs.mkdirSync(path.join(projectDir, 'test/integration/fixtures'), { recursive: true })
|
|
18
|
+
fs.writeFileSync(path.join(projectDir, 'test/integration/fixtures/smoke.dsa.ts'), 'action({}, function() {})\n')
|
|
19
|
+
fs.writeFileSync(path.join(projectDir, 'DAZStudio.exe'), '')
|
|
20
|
+
return projectDir
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
while (tempDirs.length > 0) {
|
|
25
|
+
const dir = tempDirs.pop()
|
|
26
|
+
if (dir) fs.rmSync(dir, { recursive: true, force: true })
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('integration env loader', () => {
|
|
31
|
+
it('loads simple values without overriding existing values', () => {
|
|
32
|
+
const projectDir = makeProject()
|
|
33
|
+
const envPath = path.join(projectDir, '.env.integration.local')
|
|
34
|
+
fs.writeFileSync(envPath, [
|
|
35
|
+
'# local machine settings',
|
|
36
|
+
'DAZ_STUDIO_EXE=/from/file/DAZStudio.exe',
|
|
37
|
+
'DAZ_TEST_TIMEOUT_MS="12345"',
|
|
38
|
+
'INVALID KEY=ignored',
|
|
39
|
+
'',
|
|
40
|
+
].join('\n'))
|
|
41
|
+
|
|
42
|
+
const env: Record<string, string> = {
|
|
43
|
+
DAZ_STUDIO_EXE: '/from/shell/DAZStudio.exe',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
expect(loadEnvFile(envPath, env)).toBe(true)
|
|
47
|
+
expect(env.DAZ_STUDIO_EXE).toBe('/from/shell/DAZStudio.exe')
|
|
48
|
+
expect(env.DAZ_TEST_TIMEOUT_MS).toBe('12345')
|
|
49
|
+
expect(env['INVALID KEY']).toBeUndefined()
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('integration option resolution', () => {
|
|
54
|
+
it('uses project-relative fixture and output defaults without requiring content', () => {
|
|
55
|
+
const projectDir = makeProject()
|
|
56
|
+
const options = resolveIntegrationOptions(projectDir, {
|
|
57
|
+
fixture: './test/integration/fixtures/smoke.dsa.ts',
|
|
58
|
+
}, {
|
|
59
|
+
DAZ_STUDIO_EXE: path.join(projectDir, 'DAZStudio.exe'),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
expect(options.fixturePath).toBe(path.join(projectDir, 'test/integration/fixtures/smoke.dsa.ts'))
|
|
63
|
+
expect(options.outDir).toBe(path.join(projectDir, 'test/integration/out'))
|
|
64
|
+
expect(options.fixtureRoot).toBe(path.join(projectDir, 'test/integration/out/fixture'))
|
|
65
|
+
expect(options.resultPath).toBe(path.join(projectDir, 'test/integration/out/fixture/result.json'))
|
|
66
|
+
expect(options.contentPath).toBe('')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('requires content only when requested', () => {
|
|
70
|
+
const projectDir = makeProject()
|
|
71
|
+
|
|
72
|
+
expect(() => resolveIntegrationOptions(projectDir, {
|
|
73
|
+
fixture: './test/integration/fixtures/smoke.dsa.ts',
|
|
74
|
+
requireContent: true,
|
|
75
|
+
}, {
|
|
76
|
+
DAZ_STUDIO_EXE: path.join(projectDir, 'DAZStudio.exe'),
|
|
77
|
+
})).toThrow(/DAZ_TEST_CONTENT_DUF/)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('integration result reader', () => {
|
|
82
|
+
it('accepts successful result JSON', () => {
|
|
83
|
+
const projectDir = makeProject()
|
|
84
|
+
const resultPath = path.join(projectDir, 'result.json')
|
|
85
|
+
fs.writeFileSync(resultPath, JSON.stringify({ ok: true }))
|
|
86
|
+
|
|
87
|
+
expect(readIntegrationResult(resultPath)).toEqual({ ok: true })
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('reports fixture failures', () => {
|
|
91
|
+
const projectDir = makeProject()
|
|
92
|
+
const resultPath = path.join(projectDir, 'result.json')
|
|
93
|
+
fs.writeFileSync(resultPath, JSON.stringify({ ok: false, failures: ['bad frame'] }))
|
|
94
|
+
|
|
95
|
+
expect(() => readIntegrationResult(resultPath)).toThrow(/bad frame/)
|
|
96
|
+
})
|
|
97
|
+
})
|