@astryxdesign/cli 0.6.3-canary.d0d49a4 → 0.6.3-canary.dea6813
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 +32 -18
- package/api/build/build.doc.mjs +3 -3
- package/api/build/build.test.mjs +14 -3
- package/api/build/build.type.d.mts +35 -1
- package/api/build/build.type.mjs +24 -3
- package/api/build/help/help.d.mts +8 -5
- package/api/build/help/help.mjs +56 -6
- package/api/component/component.doc.mjs +11 -2
- package/api/component/component.mjs +29 -0
- package/api/component/component.test.mjs +38 -0
- package/api/component/component.type.d.mts +11 -1
- package/api/component/component.type.mjs +8 -1
- package/api/discover/discover.doc.mjs +1 -1
- package/api/gap-report/gap-report.doc.mjs +7 -4
- package/api/init/init.doc.mjs +4 -0
- package/api/init/init.test.mjs +41 -1
- package/api/init/remove/remove.mjs +1 -1
- package/api/init/run/run.mjs +20 -10
- package/api/integration/add-contribution.component-names.test.mjs +120 -0
- package/api/integration/add-contribution.mjs +11 -0
- package/api/integration/add-contribution.test.mjs +80 -0
- package/api/integration/integrationAdd.doc.mjs +7 -4
- package/api/integration/integrationAddComponent.doc.mjs +1 -1
- package/api/integration/validate-integration.mjs +2 -0
- package/api/integration/validate-integration.test.mjs +91 -0
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/json/envelope-types.test.mjs +76 -0
- package/api/json/parseResponse.doc.mjs +2 -2
- package/api/layout/expand/expand.mjs +7 -5
- package/api/layout/expand/expand.path-safety.test.mjs +53 -0
- package/api/layout/grammar/grammar.mjs +2 -1
- package/api/layout/layoutExpand.doc.mjs +1 -1
- package/api/search/search-return-type.test.mjs +54 -0
- package/api/search/search.d.mts +2 -9
- package/api/search/search.doc.mjs +5 -1
- package/api/search/search.mjs +10 -3
- package/api/search/search.test.mjs +34 -0
- package/api/swizzle/copy/copy.mjs +28 -11
- package/api/swizzle/swizzle.doc.mjs +1 -1
- package/api/template/copy/copy.mjs +17 -23
- package/api/template/copy/copy.test.mjs +17 -0
- package/api/template/template.doc.mjs +2 -1
- package/api/theme/add/add.binary.test.mjs +91 -0
- package/api/theme/add/add.mjs +20 -3
- package/api/theme/add/add.staging.test.mjs +66 -0
- package/api/theme/build/build.mjs +114 -37
- package/api/theme/build/build.public-component-vars.test.mjs +1 -1
- package/api/theme/build/build.receipt-doc.test.mjs +111 -0
- package/api/theme/build/font-warning.mjs +3 -3
- package/api/theme/build/font-warning.test.mjs +5 -2
- package/api/theme/palette/generate/generate.mjs +7 -2
- package/api/theme/palette/generate/generate.test.mjs +96 -0
- package/api/theme/palette/generate/generator.mjs +8 -1
- package/api/theme/palette/generate/generator.test.mjs +10 -0
- package/api/theme/template/template.mjs +11 -2
- package/api/theme/template/template.test.mjs +15 -0
- package/api/theme/themeBuild.doc.mjs +7 -4
- package/api/theme/themeTemplate.doc.mjs +4 -1
- package/api/upgrade/_adapter.mjs +27 -4
- package/api/upgrade/list/list.mjs +2 -1
- package/api/upgrade/list/list.test.mjs +73 -0
- package/api/upgrade/status/status.mjs +2 -2
- package/api/upgrade/upgrade.doc.mjs +2 -1
- package/api/upgrade/upgrade.type.d.mts +4 -0
- package/api/upgrade/upgrade.type.mjs +1 -0
- package/assets/codemods/term-log.mjs +32 -8
- package/assets/codemods/term-log.test.mjs +19 -1
- package/assets/codemods/transforms/v0.6.0/__tests__/next-codemods.test.mjs +47 -0
- package/assets/codemods/transforms/v0.6.0/rename-resizable-pixel-bounds.mjs +57 -10
- package/assets/templates/pages/table-tree/page.tsx +1704 -0
- package/assets/templates/pages/table-tree/template.doc.mjs +12 -0
- package/authoring/config/config.doc.mjs +1 -1
- package/authoring/config/debug-composition.test.mjs +92 -0
- package/authoring/config/type.ts +15 -3
- package/clients/cli/command-load-failure.test.mjs +83 -0
- package/clients/cli/commands/blog.mjs +23 -8
- package/clients/cli/commands/blog.test.mjs +42 -1
- package/clients/cli/commands/build-theme.ascii-output.test.mjs +161 -0
- package/clients/cli/commands/build-theme.flag-docs.test.mjs +110 -0
- package/clients/cli/commands/build-theme.mjs +5 -5
- package/clients/cli/commands/build-theme.path-safety.test.mjs +86 -1
- package/clients/cli/commands/build-theme.variants.test.mjs +76 -0
- package/clients/cli/commands/build.doc.mjs +4 -1
- package/clients/cli/commands/build.exit-codes-doc.test.mjs +50 -0
- package/clients/cli/commands/build.mjs +32 -47
- package/clients/cli/commands/build.playbook.test.mjs +75 -0
- package/clients/cli/commands/build.text-fields.test.mjs +40 -0
- package/clients/cli/commands/component/index.mjs +2 -7
- package/clients/cli/commands/component-package.test.mjs +18 -0
- package/clients/cli/commands/component-resolution.test.mjs +21 -0
- package/clients/cli/commands/component.test.mjs +19 -0
- package/clients/cli/commands/detail-levels.test.mjs +2 -2
- package/clients/cli/commands/discover.broken-integration.test.mjs +48 -0
- package/clients/cli/commands/discover.components-flag.test.mjs +97 -0
- package/clients/cli/commands/discover.doc.mjs +4 -2
- package/clients/cli/commands/discover.mjs +3 -3
- package/clients/cli/commands/discover.text-projection.test.mjs +103 -0
- package/clients/cli/commands/doctor-integration.package-json.test.mjs +53 -0
- package/clients/cli/commands/doctor.mjs +6 -16
- package/clients/cli/commands/doctor.test.mjs +42 -0
- package/clients/cli/commands/gap-report.doc.mjs +16 -5
- package/clients/cli/commands/gap-report.test.mjs +72 -0
- package/clients/cli/commands/hook/index.mjs +7 -17
- package/clients/cli/commands/hook.text-projection.test.mjs +45 -0
- package/clients/cli/commands/init.doc.mjs +19 -8
- package/clients/cli/commands/init.flag-help.test.mjs +153 -0
- package/clients/cli/commands/integration-add.controls.test.mjs +132 -0
- package/clients/cli/commands/integration-add.doc.mjs +21 -5
- package/clients/cli/commands/interactive-guard.test.mjs +101 -24
- package/clients/cli/commands/json-contract.test.mjs +33 -0
- package/clients/cli/commands/layout-check.doc.mjs +14 -3
- package/clients/cli/commands/layout-expand.doc.mjs +21 -4
- package/clients/cli/commands/layout.doc.mjs +2 -2
- package/clients/cli/commands/layout.mjs +21 -9
- package/clients/cli/commands/layout.path-help.test.mjs +33 -0
- package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
- package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
- package/clients/cli/commands/no-prompt-wording.test.mjs +94 -0
- package/clients/cli/commands/search.doc.mjs +6 -3
- package/clients/cli/commands/search.mjs +17 -7
- package/clients/cli/commands/search.test.mjs +75 -0
- package/clients/cli/commands/swizzle.doc.mjs +2 -1
- package/clients/cli/commands/swizzle.path-safety.test.mjs +42 -0
- package/clients/cli/commands/template.doc.mjs +28 -6
- package/clients/cli/commands/template.flag-help.test.mjs +117 -0
- package/clients/cli/commands/template.path-help.test.mjs +40 -0
- package/clients/cli/commands/theme-add.doc.mjs +2 -1
- package/clients/cli/commands/theme-build.doc.mjs +7 -6
- package/clients/cli/commands/theme-palette-generate.test.mjs +19 -0
- package/clients/cli/commands/theme-template.behavior.test.mjs +12 -0
- package/clients/cli/commands/theme-template.doc.mjs +1 -1
- package/clients/cli/commands/upgrade.ascii-output.test.mjs +87 -0
- package/clients/cli/commands/upgrade.doc.mjs +17 -6
- package/clients/cli/commands/upgrade.flag-help.test.mjs +188 -0
- package/clients/cli/commands/upgrade.hook-output.test.mjs +88 -0
- package/clients/cli/index.mjs +13 -30
- package/clients/cli/latest-version-env.test.mjs +50 -0
- package/clients/cli/lib/cli-error.test.mjs +7 -0
- package/clients/cli/lib/component-format.mjs +9 -9
- package/clients/cli/lib/component-format.test.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +32 -6
- package/clients/cli/lib/doc-text-ascii.test.mjs +82 -0
- package/clients/cli/lib/exit-codes.test.mjs +97 -0
- package/clients/cli/lib/hook-format.mjs +5 -5
- package/clients/cli/lib/json-shim.mjs +38 -2
- package/clients/cli/lib/json-shim.test.mjs +83 -0
- package/clients/cli/lib/manifest.d.ts +2 -0
- package/clients/cli/lib/manifest.mjs +22 -0
- package/clients/cli/lib/manifest.test.mjs +17 -0
- package/foundation/agent-docs/agent-docs.d.mts +4 -0
- package/foundation/agent-docs/agent-docs.mjs +69 -3
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +266 -4
- package/foundation/config/integration-debug.test.mjs +28 -3
- package/foundation/config/project.mjs +123 -9
- package/foundation/config/project.test.mjs +126 -0
- package/foundation/discovery/template-adapter.d.mts +14 -5
- package/foundation/discovery/template-adapter.fixture-refs.test.mjs +248 -0
- package/foundation/discovery/template-adapter.integration-isolation.test.mjs +94 -0
- package/foundation/discovery/template-adapter.mjs +292 -25
- package/foundation/discovery/theming-targets.test.mjs +4 -0
- package/foundation/fs/module-loader.d.mts +1 -0
- package/foundation/fs/module-loader.mjs +50 -1
- package/foundation/fs/module-loader.stdout.test.mjs +332 -0
- package/foundation/fs/path-safety.d.mts +3 -2
- package/foundation/fs/path-safety.mjs +49 -19
- package/foundation/fs/path-safety.test.mjs +50 -0
- package/foundation/fs/publish-file-hardlink-unavailable.test.mjs +129 -94
- package/foundation/integrations/autolink.mjs +6 -1
- package/foundation/response/base.d.ts +8 -4
- package/foundation/response/error-codes.doc.mjs +1 -2
- package/foundation/response/error-codes.mjs +1 -1
- package/foundation/response/error-codes.test.mjs +83 -0
- package/foundation/response/json-contract.test.mjs +11 -0
- package/foundation/response/json.d.mts +4 -2
- package/foundation/response/json.mjs +8 -10
- package/foundation/response/response-types.doc.mjs +3 -3
- package/foundation/xle/expand.d.mts +2 -0
- package/foundation/xle/expand.mjs +3 -2
- package/foundation/xle/expand.test.mjs +54 -0
- package/package.json +9 -9
- package/clients/cli/lib/update-check.mjs +0 -83
- package/clients/cli/lib/update-check.test.mjs +0 -137
- package/clients/cli/update-hint-commands.test.mjs +0 -54
|
@@ -87,6 +87,54 @@ describe('astryx discover with a manifest that fails to load', () => {
|
|
|
87
87
|
});
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
describe('astryx discover with a configured package that is not installed', () => {
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
const healthy = path.join(project, 'node_modules', '@test', 'healthy');
|
|
93
|
+
fs.mkdirSync(path.join(healthy, 'components'), {recursive: true});
|
|
94
|
+
fs.writeFileSync(
|
|
95
|
+
path.join(healthy, 'package.json'),
|
|
96
|
+
JSON.stringify({name: '@test/healthy', version: '2.0.0'}),
|
|
97
|
+
);
|
|
98
|
+
fs.writeFileSync(
|
|
99
|
+
path.join(healthy, 'astryx.integration.mjs'),
|
|
100
|
+
`export default {components: './components'};\n`,
|
|
101
|
+
);
|
|
102
|
+
fs.writeFileSync(
|
|
103
|
+
path.join(healthy, 'components', 'Widget.doc.mjs'),
|
|
104
|
+
`export const docs = {name: 'Widget', usage: {description: 'A widget.'}};\n`,
|
|
105
|
+
);
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
path.join(healthy, 'components', 'Widget.tsx'),
|
|
108
|
+
`export function Widget() { return null; }\n`,
|
|
109
|
+
);
|
|
110
|
+
fs.writeFileSync(
|
|
111
|
+
path.join(project, 'astryx.config.mjs'),
|
|
112
|
+
`export default {integrations: ['@test/missing', '@test/healthy']};\n`,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('still lists the healthy package and exits 0', async () => {
|
|
117
|
+
const {status, stdout} = await runCli(['discover', '--json'], {
|
|
118
|
+
cwd: project,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(status).toBe(0);
|
|
122
|
+
expect(JSON.parse(stdout).data).toEqual([
|
|
123
|
+
expect.objectContaining({name: '@test/healthy', components: ['Widget']}),
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('names the missing package in the stderr nudge', async () => {
|
|
128
|
+
const {status, stderr} = await runCli(['discover'], {cwd: project});
|
|
129
|
+
|
|
130
|
+
expect(status).toBe(0);
|
|
131
|
+
expect(stderr).toContain(
|
|
132
|
+
'Warning: @test/missing has 1 integration issue(s). ' +
|
|
133
|
+
'Run: astryx doctor integration validate @test/missing',
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
90
138
|
describe('astryx search with a manifest that fails to load', () => {
|
|
91
139
|
// search needs a resolvable @astryxdesign/core; without one it errors out
|
|
92
140
|
// before it can list anything, which is not the case under test.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file `astryx discover --components` does what its doc says.
|
|
5
|
+
*
|
|
6
|
+
* The flag only lifts the package list's component summary (the first 10
|
|
7
|
+
* names plus "+N more") so every component prints. It changes nothing in
|
|
8
|
+
* `--json`, whose entries always carry the full array.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
16
|
+
import {doc} from './discover.doc.mjs';
|
|
17
|
+
import {doc as discoverFn} from '../../../api/discover/discover.doc.mjs';
|
|
18
|
+
|
|
19
|
+
const NAMES = Array.from({length: 12}, (_, i) => `Part${String(i).padStart(2, '0')}`);
|
|
20
|
+
|
|
21
|
+
let tmpDir;
|
|
22
|
+
let project;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-discover-components-'));
|
|
26
|
+
project = path.join(tmpDir, 'project');
|
|
27
|
+
const pkgDir = path.join(project, 'node_modules', '@test', 'kit');
|
|
28
|
+
fs.mkdirSync(path.join(pkgDir, 'components'), {recursive: true});
|
|
29
|
+
fs.writeFileSync(
|
|
30
|
+
path.join(project, 'package.json'),
|
|
31
|
+
JSON.stringify({name: 'proj', version: '1.0.0'}),
|
|
32
|
+
);
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
path.join(project, 'astryx.config.mjs'),
|
|
35
|
+
`export default {integrations: ['@test/kit']};\n`,
|
|
36
|
+
);
|
|
37
|
+
fs.writeFileSync(
|
|
38
|
+
path.join(pkgDir, 'package.json'),
|
|
39
|
+
JSON.stringify({name: '@test/kit', version: '1.0.0'}),
|
|
40
|
+
);
|
|
41
|
+
fs.writeFileSync(
|
|
42
|
+
path.join(pkgDir, 'astryx.integration.mjs'),
|
|
43
|
+
`export default {components: './components'};\n`,
|
|
44
|
+
);
|
|
45
|
+
for (const name of NAMES) {
|
|
46
|
+
fs.writeFileSync(
|
|
47
|
+
path.join(pkgDir, 'components', `${name}.doc.mjs`),
|
|
48
|
+
`export const docs = {name: '${name}', usage: {description: 'A part.'}};\n`,
|
|
49
|
+
);
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
path.join(pkgDir, 'components', `${name}.tsx`),
|
|
52
|
+
`export function ${name}() { return null; }\n`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** @param {string} stdout */
|
|
62
|
+
function componentsLine(stdout) {
|
|
63
|
+
return stdout.split('\n').find(line => line.startsWith('components:'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('astryx discover --components', () => {
|
|
67
|
+
it('lists every component where the default list shows the first 10', async () => {
|
|
68
|
+
const summary = await runCli(['discover'], {cwd: project});
|
|
69
|
+
const full = await runCli(['discover', '--components'], {cwd: project});
|
|
70
|
+
|
|
71
|
+
expect(componentsLine(summary.stdout)).toMatch(
|
|
72
|
+
new RegExp(`${NAMES[9]}, \\+2 more$`),
|
|
73
|
+
);
|
|
74
|
+
expect(componentsLine(full.stdout)).toMatch(new RegExp(`${NAMES.join(', ')}$`));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('leaves the --json response unchanged', async () => {
|
|
78
|
+
const plain = await runCli(['discover', '--json'], {cwd: project});
|
|
79
|
+
const flagged = await runCli(['discover', '--components', '--json'], {
|
|
80
|
+
cwd: project,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(JSON.parse(flagged.stdout)).toEqual(JSON.parse(plain.stdout));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('is documented as that behavior, with an example', () => {
|
|
87
|
+
const option = doc.options?.find(o => o.flag === '--components');
|
|
88
|
+
const param = discoverFn.params?.find(p => p.name === 'options.components');
|
|
89
|
+
|
|
90
|
+
for (const description of [option?.description, param?.description]) {
|
|
91
|
+
expect(description).toMatch(/every component/i);
|
|
92
|
+
expect(description).toMatch(/first 10/);
|
|
93
|
+
expect(description).not.toMatch(/components only/i);
|
|
94
|
+
}
|
|
95
|
+
expect(doc.examples?.some(e => /\s--components\b/.test(e.cli))).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -25,18 +25,20 @@ export const doc = {
|
|
|
25
25
|
{
|
|
26
26
|
flag: '--components',
|
|
27
27
|
param: 'options.components',
|
|
28
|
-
description:
|
|
28
|
+
description:
|
|
29
|
+
'In the package list, print every component of each package instead of the first 10 and a "+N more" count. No effect on --json output or on a package, component, or search query.',
|
|
29
30
|
},
|
|
30
31
|
],
|
|
31
32
|
examples: [
|
|
32
33
|
{label: 'List packages', cli: 'astryx discover --json'},
|
|
34
|
+
{label: 'List every component of each package', cli: 'astryx discover --components'},
|
|
33
35
|
{label: 'Browse a package', cli: 'astryx discover @acme/ui'},
|
|
34
36
|
],
|
|
35
37
|
exitCodes: [
|
|
36
38
|
{code: 0, when: 'success'},
|
|
37
39
|
{
|
|
38
40
|
code: 1,
|
|
39
|
-
when: 'unknown package or component, a malformed doc, or
|
|
41
|
+
when: 'unknown package or component, a malformed doc, or a blank query when packages are discovered',
|
|
40
42
|
},
|
|
41
43
|
],
|
|
42
44
|
related: ['component', 'search', 'template'],
|
|
@@ -122,11 +122,11 @@ export function registerDiscover(program) {
|
|
|
122
122
|
break;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// One record per package
|
|
125
|
+
// One record per package, every field of the JSON entry. The default
|
|
126
126
|
// view summarizes each package's components (first N + "+N more");
|
|
127
127
|
// --components lists them all (record comma-joins the full array).
|
|
128
128
|
/** @type {import('../formatters/index.mjs').RecordOptions} */
|
|
129
|
-
const listOpts = {
|
|
129
|
+
const listOpts = {};
|
|
130
130
|
if (!options.components) {
|
|
131
131
|
listOpts.format = {
|
|
132
132
|
components: (/** @type {string[]} */ comps) => {
|
|
@@ -155,7 +155,7 @@ export function registerDiscover(program) {
|
|
|
155
155
|
case 'discover.detail': {
|
|
156
156
|
const d = result.data;
|
|
157
157
|
emit(
|
|
158
|
-
record(d
|
|
158
|
+
record(d),
|
|
159
159
|
text(`Usage: ${run} discover ${d.name}/<ComponentName>`),
|
|
160
160
|
);
|
|
161
161
|
break;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file `astryx discover` text output projects every field of its JSON entry.
|
|
5
|
+
*
|
|
6
|
+
* The text views of `discover.list` and `discover.detail` are renderings of the
|
|
7
|
+
* same entries `--json` returns, so each entry key must appear as a `key:` line
|
|
8
|
+
* carrying the same value.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
16
|
+
|
|
17
|
+
let tmpDir;
|
|
18
|
+
let project;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-discover-text-'));
|
|
22
|
+
project = path.join(tmpDir, 'project');
|
|
23
|
+
const pkgDir = path.join(project, 'node_modules', '@test', 'kit');
|
|
24
|
+
fs.mkdirSync(path.join(pkgDir, 'components'), {recursive: true});
|
|
25
|
+
fs.writeFileSync(
|
|
26
|
+
path.join(project, 'package.json'),
|
|
27
|
+
JSON.stringify({name: 'proj', version: '1.0.0'}),
|
|
28
|
+
);
|
|
29
|
+
fs.writeFileSync(
|
|
30
|
+
path.join(project, 'astryx.config.mjs'),
|
|
31
|
+
`export default {integrations: ['@test/kit']};\n`,
|
|
32
|
+
);
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
path.join(pkgDir, 'package.json'),
|
|
35
|
+
JSON.stringify({name: '@test/kit', version: '3.1.4'}),
|
|
36
|
+
);
|
|
37
|
+
fs.writeFileSync(
|
|
38
|
+
path.join(pkgDir, 'astryx.integration.mjs'),
|
|
39
|
+
`export default {components: './components'};\n`,
|
|
40
|
+
);
|
|
41
|
+
for (const name of ['Dial', 'Gauge']) {
|
|
42
|
+
fs.writeFileSync(
|
|
43
|
+
path.join(pkgDir, 'components', `${name}.doc.mjs`),
|
|
44
|
+
`export const docs = {name: '${name}', usage: {description: 'A ${name}.'}};\n`,
|
|
45
|
+
);
|
|
46
|
+
fs.writeFileSync(
|
|
47
|
+
path.join(pkgDir, 'components', `${name}.tsx`),
|
|
48
|
+
`export function ${name}() { return null; }\n`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {Record<string, unknown>} entry
|
|
59
|
+
* @param {string} text
|
|
60
|
+
*/
|
|
61
|
+
function expectEveryField(entry, text) {
|
|
62
|
+
const lines = text.split('\n');
|
|
63
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
64
|
+
const rendered = Array.isArray(value) ? value.join(', ') : String(value);
|
|
65
|
+
expect(lines).toContainEqual(
|
|
66
|
+
expect.stringMatching(new RegExp(`^${key}:\\s+${escape(rendered)}$`)),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @param {string} s */
|
|
72
|
+
function escape(s) {
|
|
73
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('astryx discover text output', () => {
|
|
77
|
+
it('lists every field of each discover.list entry', async () => {
|
|
78
|
+
const asJson = await runCli(['discover', '--json'], {cwd: project});
|
|
79
|
+
const [entry] = JSON.parse(asJson.stdout).data;
|
|
80
|
+
expect(Object.keys(entry)).toEqual(
|
|
81
|
+
expect.arrayContaining(['name', 'category', 'components', 'version']),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const {status, stdout} = await runCli(['discover'], {cwd: project});
|
|
85
|
+
|
|
86
|
+
expect(status).toBe(0);
|
|
87
|
+
expectEveryField(entry, stdout);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('shows every field of the discover.detail entry', async () => {
|
|
91
|
+
const asJson = await runCli(['discover', '@test/kit', '--json'], {
|
|
92
|
+
cwd: project,
|
|
93
|
+
});
|
|
94
|
+
const {data} = JSON.parse(asJson.stdout);
|
|
95
|
+
|
|
96
|
+
const {status, stdout} = await runCli(['discover', '@test/kit'], {
|
|
97
|
+
cwd: project,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(status).toBe(0);
|
|
101
|
+
expectEveryField(data, stdout);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
7
|
+
import {validateIntegration} from '../../../api/integration/validate-integration.mjs';
|
|
8
|
+
|
|
9
|
+
const NO_MANIFEST = 'No astryx.integration.* found next to package.json.';
|
|
10
|
+
|
|
11
|
+
let tmpDir;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-doctor-pkgjson-'));
|
|
15
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name": "@acme/widgets",}\n');
|
|
16
|
+
fs.writeFileSync(
|
|
17
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
18
|
+
"export default {\n templates: './templates',\n};\n",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('doctor integration with a malformed package.json', () => {
|
|
27
|
+
it('keeps a null name for the no-manifest case only', async () => {
|
|
28
|
+
const result = await validateIntegration(undefined, {cwd: tmpDir});
|
|
29
|
+
|
|
30
|
+
expect(result.data.name).toBe('(local package)');
|
|
31
|
+
expect(result.data.issues.map(issue => issue.code)).toEqual([
|
|
32
|
+
'invalid_package_json',
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
for (const leaf of ['validate', 'templates', 'components', 'docs']) {
|
|
37
|
+
it(`${leaf} prints the package.json error instead of "no manifest"`, async () => {
|
|
38
|
+
const text = await runCli(['doctor', 'integration', leaf], tmpDir);
|
|
39
|
+
const json = await runCli(
|
|
40
|
+
['doctor', 'integration', leaf, '--json'],
|
|
41
|
+
tmpDir,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(text.status).toBe(1);
|
|
45
|
+
expect(json.status).toBe(1);
|
|
46
|
+
expect(text.stdout).not.toContain(NO_MANIFEST);
|
|
47
|
+
expect(text.stdout).toContain('invalid_package_json');
|
|
48
|
+
expect(JSON.parse(json.stdout).data.issues).toContainEqual(
|
|
49
|
+
expect.objectContaining({code: 'invalid_package_json'}),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
validateIntegration,
|
|
18
18
|
} from '../../../api/integration/validate-integration.mjs';
|
|
19
19
|
import {jsonOut} from '../../../foundation/response/json.mjs';
|
|
20
|
-
import {emit, section, records, text} from '../formatters/index.mjs';
|
|
20
|
+
import {emit, section, record, records, text} from '../formatters/index.mjs';
|
|
21
21
|
import {defineCommand} from '../lib/define-command.mjs';
|
|
22
22
|
import {doc as doctorCommand} from './doctor.doc.mjs';
|
|
23
23
|
import {doc as doctorIntegrationGroup} from './doctor-integration.doc.mjs';
|
|
@@ -55,7 +55,7 @@ function integrationLabel(data) {
|
|
|
55
55
|
* @param {import('../../../api/doctor/doctor.mjs').DoctorReport} report
|
|
56
56
|
*/
|
|
57
57
|
function printHuman(report) {
|
|
58
|
-
const {
|
|
58
|
+
const {warn, fail} = report.summary;
|
|
59
59
|
const closing =
|
|
60
60
|
fail > 0
|
|
61
61
|
? 'Some checks failed. Address the items marked [fail] above.'
|
|
@@ -63,18 +63,15 @@ function printHuman(report) {
|
|
|
63
63
|
? 'No failures — but review the [warn] warnings above when you can.'
|
|
64
64
|
: 'All checks passed. Your Astryx setup looks healthy.';
|
|
65
65
|
|
|
66
|
+
// Field names are the JSON keys, so text and --json map one to one.
|
|
66
67
|
emit(
|
|
67
68
|
section('astryx doctor — diagnosing your setup'),
|
|
68
69
|
records(report.checks, {
|
|
69
|
-
fields: ['status', 'label', 'message', 'fix'],
|
|
70
|
-
labels: {label: 'check'},
|
|
70
|
+
fields: ['id', 'status', 'label', 'message', 'fix'],
|
|
71
71
|
format: {status: statusToken},
|
|
72
72
|
}),
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
`${fail} failure${fail === 1 ? '' : 's'}` +
|
|
76
|
-
(info ? `, ${info} info` : ''),
|
|
77
|
-
),
|
|
73
|
+
section('summary'),
|
|
74
|
+
record(report.summary, {fields: ['pass', 'warn', 'fail', 'info']}),
|
|
78
75
|
text(closing),
|
|
79
76
|
);
|
|
80
77
|
}
|
|
@@ -290,13 +287,6 @@ export function registerDoctor(program) {
|
|
|
290
287
|
fn: doctorFn,
|
|
291
288
|
action: async () => runProjectDoctor(program),
|
|
292
289
|
});
|
|
293
|
-
doctorCmd.addHelpText(
|
|
294
|
-
'after',
|
|
295
|
-
'\nExit code:\n' +
|
|
296
|
-
' 0 no failures (warnings are allowed) — safe as a CI gate\n' +
|
|
297
|
-
' 1 one or more checks failed\n',
|
|
298
|
-
);
|
|
299
|
-
|
|
300
290
|
/** @type {import('commander').Command} */
|
|
301
291
|
let integrationCmd;
|
|
302
292
|
integrationCmd = defineCommand(doctorCmd, doctorIntegrationGroup, {
|
|
@@ -354,3 +354,45 @@ describe('doctor — command', () => {
|
|
|
354
354
|
}
|
|
355
355
|
});
|
|
356
356
|
});
|
|
357
|
+
|
|
358
|
+
describe('doctor — text output mirrors the JSON', () => {
|
|
359
|
+
/** @param {string} line */
|
|
360
|
+
const field = line => /^(\w+):\s+(.*)$/.exec(line)?.slice(1) ?? [line, ''];
|
|
361
|
+
|
|
362
|
+
it('prints every check and summary field under its JSON key', async () => {
|
|
363
|
+
const prevCwd = process.cwd();
|
|
364
|
+
const prevExit = process.exitCode;
|
|
365
|
+
process.chdir(tmpDir);
|
|
366
|
+
try {
|
|
367
|
+
await createProgram().parseAsync(['node', 'astryx', '--json', 'doctor']);
|
|
368
|
+
const {checks, summary} = JSON.parse(logCalls.join('\n')).data;
|
|
369
|
+
logCalls = [];
|
|
370
|
+
await createProgram().parseAsync(['node', 'astryx', 'doctor']);
|
|
371
|
+
const blocks = logCalls
|
|
372
|
+
.join('\n')
|
|
373
|
+
.split(/\n{2,}/)
|
|
374
|
+
.map(block => block.split('\n').map(field));
|
|
375
|
+
|
|
376
|
+
for (const check of checks) {
|
|
377
|
+
const block = blocks.find(fields =>
|
|
378
|
+
fields.some(([key, value]) => key === 'id' && value === check.id),
|
|
379
|
+
);
|
|
380
|
+
expect(block, `no text record for check ${check.id}`).toBeDefined();
|
|
381
|
+
expect(
|
|
382
|
+
/** @type {string[][]} */ (block).map(([key]) => key).sort(),
|
|
383
|
+
).toEqual(Object.keys(check).sort());
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const heading = blocks.findIndex(
|
|
387
|
+
fields => fields.length === 1 && fields[0][0] === 'summary',
|
|
388
|
+
);
|
|
389
|
+
expect(heading, 'no summary section').toBeGreaterThan(-1);
|
|
390
|
+
expect(blocks[heading + 1]).toEqual(
|
|
391
|
+
Object.entries(summary).map(([key, count]) => [key, String(count)]),
|
|
392
|
+
);
|
|
393
|
+
} finally {
|
|
394
|
+
process.chdir(prevCwd);
|
|
395
|
+
process.exitCode = prevExit;
|
|
396
|
+
}
|
|
397
|
+
}, 60_000);
|
|
398
|
+
});
|
|
@@ -15,22 +15,32 @@ export const doc = {
|
|
|
15
15
|
description:
|
|
16
16
|
'Reports a missing component, variant, layout, styling, accessibility, API, or documentation capability. The command selects an explicit package first, then a unique component owner, then Core. The report fans out to every effective handler: project config first, then each loaded integration in config order. Public handlers require --confirm-public per handler; internal handlers always run. A handler failure is isolated and does not prevent later handlers.',
|
|
17
17
|
fn: 'gapReport',
|
|
18
|
-
args: [
|
|
18
|
+
args: [
|
|
19
|
+
{
|
|
20
|
+
name: 'component',
|
|
21
|
+
param: 'component',
|
|
22
|
+
required: false,
|
|
23
|
+
description:
|
|
24
|
+
'Component or design-system area the gap is about, up to 120 characters. Required unless --list-categories is set.',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
19
27
|
options: [
|
|
20
28
|
{
|
|
21
29
|
flag: '--category <category>',
|
|
22
30
|
param: 'options.category',
|
|
23
|
-
description:
|
|
31
|
+
description:
|
|
32
|
+
'Gap category (run --list-categories for values). Required unless --list-categories is set.',
|
|
24
33
|
},
|
|
25
34
|
{
|
|
26
35
|
flag: '--reason <reason>',
|
|
27
36
|
param: 'options.reason',
|
|
28
|
-
description:
|
|
37
|
+
description:
|
|
38
|
+
'What capability was missing or difficult, up to 2000 characters. Required unless --list-categories is set.',
|
|
29
39
|
},
|
|
30
40
|
{
|
|
31
41
|
flag: '--additional-context <text>',
|
|
32
42
|
param: 'options.detail',
|
|
33
|
-
description: 'Optional additional context',
|
|
43
|
+
description: 'Optional additional context, up to 8000 characters',
|
|
34
44
|
},
|
|
35
45
|
{
|
|
36
46
|
flag: '--package <pkg>',
|
|
@@ -45,7 +55,8 @@ export const doc = {
|
|
|
45
55
|
{
|
|
46
56
|
flag: '--list-categories',
|
|
47
57
|
param: 'options.listCategories',
|
|
48
|
-
description:
|
|
58
|
+
description:
|
|
59
|
+
'List valid report categories without filing; the component and the other gap-report options are ignored',
|
|
49
60
|
},
|
|
50
61
|
],
|
|
51
62
|
examples: [
|
|
@@ -196,3 +196,75 @@ export default {};\n`,
|
|
|
196
196
|
});
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
|
+
|
|
200
|
+
describe('gap-report control docs', () => {
|
|
201
|
+
const REQUIRED = 'Required unless --list-categories is set.';
|
|
202
|
+
|
|
203
|
+
it('help and manifest say which inputs are required and how long they may be', async () => {
|
|
204
|
+
const help = await runCli(['gap-report', '--help']);
|
|
205
|
+
expect(help.status).toBe(0);
|
|
206
|
+
const manifest = JSON.parse((await runCli(['manifest', '--json'])).stdout);
|
|
207
|
+
const entry = manifest.data.commands.find(
|
|
208
|
+
(/** @type {{name: string}} */ command) => command.name === 'gap-report',
|
|
209
|
+
);
|
|
210
|
+
/** @param {string} flag @returns {string} */
|
|
211
|
+
const option = flag =>
|
|
212
|
+
entry.options.find(
|
|
213
|
+
(/** @type {{flag: string}} */ item) => item.flag.split(' ')[0] === flag,
|
|
214
|
+
)?.description ?? '';
|
|
215
|
+
const component = entry.arguments[0].description;
|
|
216
|
+
|
|
217
|
+
for (const text of [component, option('--category'), option('--reason')]) {
|
|
218
|
+
expect(text).toContain(REQUIRED);
|
|
219
|
+
expect(help.stdout).toContain(text);
|
|
220
|
+
}
|
|
221
|
+
expect(option('--list-categories')).toContain(
|
|
222
|
+
'the component and the other gap-report options are ignored',
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-gap-limits-'));
|
|
226
|
+
try {
|
|
227
|
+
fs.writeFileSync(
|
|
228
|
+
path.join(cwd, 'package.json'),
|
|
229
|
+
JSON.stringify({name: 'fixture-app'}),
|
|
230
|
+
);
|
|
231
|
+
const base = {cwd, category: /** @type {const} */ ('docs_gap'), reason: 'x'};
|
|
232
|
+
/** @type {Array<[string, (value: string) => Promise<unknown>]>} */
|
|
233
|
+
const limits = [
|
|
234
|
+
[component, value => gapReport(value, base)],
|
|
235
|
+
[option('--reason'), value => gapReport('Button', {...base, reason: value})],
|
|
236
|
+
[
|
|
237
|
+
option('--additional-context'),
|
|
238
|
+
value => gapReport('Button', {...base, detail: value}),
|
|
239
|
+
],
|
|
240
|
+
];
|
|
241
|
+
for (const [text, call] of limits) {
|
|
242
|
+
const limit = Number(/up to (\d+) characters/.exec(text)?.[1]);
|
|
243
|
+
expect(limit, text).toBeGreaterThan(0);
|
|
244
|
+
await expect(call('x'.repeat(limit))).resolves.toMatchObject({
|
|
245
|
+
type: 'gap-report.file',
|
|
246
|
+
});
|
|
247
|
+
await expect(call('x'.repeat(limit + 1))).rejects.toMatchObject({
|
|
248
|
+
code: 'ERR_INVALID_ARGUMENT',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
} finally {
|
|
252
|
+
fs.rmSync(cwd, {recursive: true, force: true});
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('enforces the documented requirement', async () => {
|
|
257
|
+
for (const [component, options] of [
|
|
258
|
+
[undefined, {category: 'docs_gap', reason: 'x'}],
|
|
259
|
+
['Button', {reason: 'x'}],
|
|
260
|
+
['Button', {category: 'docs_gap'}],
|
|
261
|
+
]) {
|
|
262
|
+
await expect(gapReport(component, options)).rejects.toMatchObject({
|
|
263
|
+
code: 'ERR_MISSING_ARGUMENT',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
await expect(
|
|
267
|
+
gapReport(undefined, {listCategories: true, category: 'not_real', package: 'nope'}),
|
|
268
|
+
).resolves.toMatchObject({type: 'gap-report.categories'});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -20,7 +20,6 @@ import {defineCommand} from '../../lib/define-command.mjs';
|
|
|
20
20
|
import {resultSet} from '../../../../foundation/debug/index.mjs';
|
|
21
21
|
import {ERROR_CODES} from '../../../../foundation/response/error-codes.mjs';
|
|
22
22
|
import {hook as hookApi} from '../../../../api/hook/hook.mjs';
|
|
23
|
-
import {findRelatedBlocks} from '../../../../api/template/template.mjs';
|
|
24
23
|
import {doc as hookCommand} from '../hook.doc.mjs';
|
|
25
24
|
import {doc as hookFn} from '../../../../api/hook/hook.doc.mjs';
|
|
26
25
|
|
|
@@ -169,24 +168,15 @@ export function registerHook(program) {
|
|
|
169
168
|
)
|
|
170
169
|
: formatHookFull(result.data);
|
|
171
170
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const allBlocks = [];
|
|
176
|
-
for (const comp of relatedComps) {
|
|
177
|
-
const blocks = await findRelatedBlocks(comp);
|
|
178
|
-
for (const b of blocks) {
|
|
179
|
-
if (!allBlocks.some(existing => existing.dirName === b.dirName)) {
|
|
180
|
-
allBlocks.push(b);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
171
|
+
// Text projects the envelope only. Block templates are JSON-backed
|
|
172
|
+
// under `component <name> --blocks`.
|
|
173
|
+
const related = result.data.relatedComponents ?? [];
|
|
185
174
|
emit(
|
|
186
175
|
code(doc),
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
176
|
+
related.length > 0 &&
|
|
177
|
+
text(
|
|
178
|
+
`Block templates: ${run} component <name> --blocks for ${related.join(', ')}`,
|
|
179
|
+
),
|
|
190
180
|
);
|
|
191
181
|
break;
|
|
192
182
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file `astryx hook <name>` text is a projection of `hook.detail`: it shows
|
|
5
|
+
* nothing the JSON envelope does not carry.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {describe, it, expect} from 'vitest';
|
|
9
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
10
|
+
import {findRelatedBlocks} from '../../../api/template/template.mjs';
|
|
11
|
+
|
|
12
|
+
const HOOK = 'useAnnounce';
|
|
13
|
+
|
|
14
|
+
describe('hook detail text projection', () => {
|
|
15
|
+
it('prints no block templates that the JSON envelope lacks', async () => {
|
|
16
|
+
const json = await runCli(['--json', 'hook', HOOK]);
|
|
17
|
+
expect(json.status).toBe(0);
|
|
18
|
+
const envelope = JSON.parse(json.stdout);
|
|
19
|
+
expect(envelope.type).toBe('hook.detail');
|
|
20
|
+
const related = envelope.data.relatedComponents;
|
|
21
|
+
expect(related.length).toBeGreaterThan(0);
|
|
22
|
+
|
|
23
|
+
const blockNames = [];
|
|
24
|
+
for (const component of related) {
|
|
25
|
+
for (const block of await findRelatedBlocks(component)) {
|
|
26
|
+
blockNames.push(block.dirName);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
expect(blockNames.length).toBeGreaterThan(0);
|
|
30
|
+
|
|
31
|
+
const text = await runCli(['hook', HOOK]);
|
|
32
|
+
expect(text.status).toBe(0);
|
|
33
|
+
const shownButNotInJson = blockNames.filter(
|
|
34
|
+
name => text.stdout.includes(name) && !json.stdout.includes(name),
|
|
35
|
+
);
|
|
36
|
+
expect(shownButNotInJson).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('points at the JSON-backed block lookup for each related component', async () => {
|
|
40
|
+
const text = await runCli(['hook', HOOK]);
|
|
41
|
+
expect(text.stdout).toMatch(/component <name> --blocks/);
|
|
42
|
+
expect(text.stdout).toContain('VisuallyHidden');
|
|
43
|
+
expect(text.stdout).toContain('Toast');
|
|
44
|
+
});
|
|
45
|
+
});
|