9840f17189
- src/components/image-editor/model3d-generation/Model3dGenerationValidation.ts:isModel3dModelVersion 改用 Object.hasOwn,`toString` / `__proto__` 这类继承键不再被判成契约版本(之后查表会拿到 undefined) - src/components/image-editor/model3d-generation/Model3dGenerationFormModel.test.ts:补原型链键回落默认版本的用例
605 lines
18 KiB
TypeScript
605 lines
18 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import type { Model3dTextureQuality } from '../../../../packages/shared/src/contracts/model3d';
|
|
import type {
|
|
Model3dAddOnPriceKey,
|
|
Model3dPricingConfig,
|
|
} from '../../../services/image-editor/editorProjectClient';
|
|
import type { GenerateDialogState } from '../ImageCanvasEditorTypes';
|
|
import {
|
|
applyModel3dModelChange,
|
|
applyModel3dPromptChange,
|
|
applyModel3dReferenceRemoval,
|
|
applyModel3dSwitchChange,
|
|
applyModel3dTierChange,
|
|
createModel3dGenerationDraftFields,
|
|
DEFAULT_MODEL3D_MODEL_VERSION,
|
|
mintModel3dAttemptNonce,
|
|
MODEL3D_PRICING_UNAVAILABLE_REASON,
|
|
type Model3dGenerationMode,
|
|
normalizeModel3dModelVersion,
|
|
refreshModel3dAttemptNonce,
|
|
resolveModel3dAddOnKeys,
|
|
resolveModel3dGenerationParams,
|
|
resolveModel3dGenerationQuote,
|
|
resolveModel3dModelOptions,
|
|
resolveModel3dModelVersion,
|
|
resolveModel3dPrice,
|
|
} from './Model3dGenerationFormModel';
|
|
|
|
function createDialog(
|
|
overrides: Partial<GenerateDialogState> = {},
|
|
): GenerateDialogState {
|
|
return {
|
|
id: 'dialog-3d',
|
|
...createModel3dGenerationDraftFields('model3d-text-to-model'),
|
|
...overrides,
|
|
} as GenerateDialogState;
|
|
}
|
|
|
|
/**
|
|
* 与后端 `api-server::tripo3d::pricing::model3d_add_ons` 逐条对齐的期望值:
|
|
* 前端只有这一份映射,价格差异必须在这里被用例挡住。
|
|
*/
|
|
function expectedAddOns({
|
|
texture,
|
|
textureQuality,
|
|
geometryQuality,
|
|
quad,
|
|
smartLowPoly,
|
|
generateParts,
|
|
}: {
|
|
texture: boolean;
|
|
textureQuality: Model3dTextureQuality;
|
|
geometryQuality: 'standard' | 'detailed';
|
|
quad: boolean;
|
|
smartLowPoly: boolean;
|
|
generateParts: boolean;
|
|
}): Model3dAddOnPriceKey[] {
|
|
return [
|
|
...(texture && textureQuality === 'detailed' ? ['hdTexture' as const] : []),
|
|
...(texture && textureQuality === 'extreme'
|
|
? ['ultraTexture' as const]
|
|
: []),
|
|
...(geometryQuality === 'detailed' ? ['hdGeometry' as const] : []),
|
|
...(quad ? ['quadMesh' as const] : []),
|
|
...(smartLowPoly ? ['smartLowPoly' as const] : []),
|
|
...(generateParts ? ['generateParts' as const] : []),
|
|
];
|
|
}
|
|
|
|
const PRICING: Model3dPricingConfig = {
|
|
basePrices: {
|
|
'text-to-model': {
|
|
'v3.1-20260211': { noTexture: 100, texture: 200 },
|
|
},
|
|
'image-to-model': {
|
|
'v3.1-20260211': { noTexture: 150, texture: 250 },
|
|
},
|
|
},
|
|
addOnPrices: {
|
|
hdTexture: 10,
|
|
ultraTexture: 20,
|
|
hdGeometry: 30,
|
|
quadMesh: 40,
|
|
smartLowPoly: 50,
|
|
generateParts: 60,
|
|
},
|
|
};
|
|
|
|
describe('resolveModel3dGenerationParams', () => {
|
|
it('每个档位映射成完整参数,档位与开关不留给 provider 隐式默认', () => {
|
|
expect(
|
|
resolveModel3dGenerationParams({
|
|
model: DEFAULT_MODEL3D_MODEL_VERSION,
|
|
tier: 'textured-standard',
|
|
quad: false,
|
|
smartLowPoly: false,
|
|
generateParts: false,
|
|
}),
|
|
).toEqual({
|
|
ok: true,
|
|
params: {
|
|
texture: true,
|
|
textureQuality: 'standard',
|
|
geometryQuality: 'standard',
|
|
quad: false,
|
|
smartLowPoly: false,
|
|
generateParts: false,
|
|
pbr: false,
|
|
},
|
|
});
|
|
expect(
|
|
resolveModel3dGenerationParams({
|
|
model: DEFAULT_MODEL3D_MODEL_VERSION,
|
|
tier: 'textured-detailed',
|
|
quad: true,
|
|
smartLowPoly: true,
|
|
generateParts: false,
|
|
}),
|
|
).toEqual({
|
|
ok: true,
|
|
params: {
|
|
texture: true,
|
|
textureQuality: 'detailed',
|
|
geometryQuality: 'detailed',
|
|
quad: true,
|
|
smartLowPoly: true,
|
|
generateParts: false,
|
|
pbr: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('纯几何档位没有贴图档位,pbr 显式为 false', () => {
|
|
const result = resolveModel3dGenerationParams({
|
|
model: DEFAULT_MODEL3D_MODEL_VERSION,
|
|
tier: 'geometry-only',
|
|
quad: false,
|
|
smartLowPoly: false,
|
|
generateParts: true,
|
|
});
|
|
expect(result.ok).toBe(true);
|
|
expect(result.ok && result.params).toMatchObject({
|
|
texture: false,
|
|
textureQuality: null,
|
|
geometryQuality: 'standard',
|
|
generateParts: true,
|
|
pbr: false,
|
|
});
|
|
});
|
|
|
|
it('贴图档位不允许分件,这是后端也会拒的组合', () => {
|
|
expect(
|
|
resolveModel3dGenerationParams({
|
|
model: DEFAULT_MODEL3D_MODEL_VERSION,
|
|
tier: 'textured-standard',
|
|
quad: false,
|
|
smartLowPoly: false,
|
|
generateParts: true,
|
|
}).ok,
|
|
).toBe(false);
|
|
expect(
|
|
resolveModel3dGenerationParams({
|
|
model: DEFAULT_MODEL3D_MODEL_VERSION,
|
|
tier: 'textured-detailed',
|
|
quad: false,
|
|
smartLowPoly: false,
|
|
generateParts: true,
|
|
}).ok,
|
|
).toBe(false);
|
|
});
|
|
|
|
it('分件不允许与方形布线 / 智能低多边形同时开,这是后端也会拒的组合', () => {
|
|
const conflictingSwitches: Array<{ quad: boolean; smartLowPoly: boolean }> =
|
|
[
|
|
{ quad: true, smartLowPoly: false },
|
|
{ quad: false, smartLowPoly: true },
|
|
];
|
|
for (const switches of conflictingSwitches) {
|
|
expect(
|
|
resolveModel3dGenerationParams({
|
|
model: DEFAULT_MODEL3D_MODEL_VERSION,
|
|
tier: 'geometry-only',
|
|
quad: switches.quad,
|
|
smartLowPoly: switches.smartLowPoly,
|
|
generateParts: true,
|
|
}),
|
|
).toEqual({
|
|
ok: false,
|
|
reason: '分件生成不能与方形布线或智能低多边形同时使用',
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('resolveModel3dAddOnKeys', () => {
|
|
it('与后端 add-on 判定在全部档位与开关组合上一致', () => {
|
|
for (const tier of [
|
|
'textured-standard',
|
|
'textured-detailed',
|
|
'geometry-only',
|
|
] as const) {
|
|
for (const quad of [false, true]) {
|
|
for (const smartLowPoly of [false, true]) {
|
|
for (const generateParts of [false, true]) {
|
|
const result = resolveModel3dGenerationParams({
|
|
model: DEFAULT_MODEL3D_MODEL_VERSION,
|
|
tier,
|
|
quad,
|
|
smartLowPoly,
|
|
generateParts,
|
|
});
|
|
if (!result.ok) {
|
|
continue;
|
|
}
|
|
expect(resolveModel3dAddOnKeys(result.params)).toEqual(
|
|
expectedAddOns({
|
|
texture: result.params.texture,
|
|
textureQuality: result.params.textureQuality ?? 'standard',
|
|
geometryQuality: result.params.geometryQuality,
|
|
quad,
|
|
smartLowPoly,
|
|
generateParts,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
const MULTI_VERSION_PRICING: Model3dPricingConfig = {
|
|
basePrices: {
|
|
'text-to-model': {
|
|
'v3.1-20260211': { noTexture: 100, texture: 200 },
|
|
'v2.5-20250123': { noTexture: 60, texture: 120 },
|
|
'P2-20260801': { noTexture: 80, texture: 160 },
|
|
},
|
|
'image-to-model': {
|
|
'v3.1-20260211': { noTexture: 150, texture: 250 },
|
|
'P2-20260801': { noTexture: 170, texture: 270 },
|
|
},
|
|
},
|
|
addOnPrices: {
|
|
hdTexture: 10,
|
|
ultraTexture: 20,
|
|
hdGeometry: 30,
|
|
quadMesh: 40,
|
|
smartLowPoly: 50,
|
|
generateParts: 60,
|
|
},
|
|
};
|
|
|
|
describe('模型版本', () => {
|
|
it('缺字段与非法取值都回落到默认版本', () => {
|
|
expect(resolveModel3dModelVersion(undefined)).toBe(
|
|
DEFAULT_MODEL3D_MODEL_VERSION,
|
|
);
|
|
expect(normalizeModel3dModelVersion(' P2-20260801 ')).toBe('P2-20260801');
|
|
// 老快照里的退役版本不发给服务端。
|
|
expect(normalizeModel3dModelVersion('v1.0-20190101')).toBe(
|
|
DEFAULT_MODEL3D_MODEL_VERSION,
|
|
);
|
|
expect(normalizeModel3dModelVersion(null)).toBe(
|
|
DEFAULT_MODEL3D_MODEL_VERSION,
|
|
);
|
|
// 原型链上的键(`in` 会命中)不是契约版本,同样按默认版本处理。
|
|
for (const inheritedKey of ['toString', 'constructor', '__proto__']) {
|
|
expect(normalizeModel3dModelVersion(inheritedKey)).toBe(
|
|
DEFAULT_MODEL3D_MODEL_VERSION,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('可选项只给当前端点有底价的版本,顺序按契约声明', () => {
|
|
expect(
|
|
resolveModel3dModelOptions({
|
|
pricing: MULTI_VERSION_PRICING,
|
|
endpoint: 'text-to-model',
|
|
}),
|
|
).toEqual([
|
|
{ value: DEFAULT_MODEL3D_MODEL_VERSION, label: 'v3.1' },
|
|
{ value: 'v2.5-20250123', label: 'v2.5' },
|
|
{ value: 'P2-20260801', label: 'P2' },
|
|
]);
|
|
expect(
|
|
resolveModel3dModelOptions({
|
|
pricing: MULTI_VERSION_PRICING,
|
|
endpoint: 'image-to-model',
|
|
}),
|
|
).toEqual([
|
|
{ value: DEFAULT_MODEL3D_MODEL_VERSION, label: 'v3.1' },
|
|
{ value: 'P2-20260801', label: 'P2' },
|
|
]);
|
|
expect(
|
|
resolveModel3dModelOptions({ pricing: null, endpoint: 'text-to-model' }),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('报价按对话框选中的版本取底价', () => {
|
|
expect(
|
|
resolveModel3dGenerationQuote(
|
|
createDialog({ model3dModel: 'P2-20260801' }),
|
|
MULTI_VERSION_PRICING,
|
|
),
|
|
).toEqual({ ok: true, price: 160, endpoint: 'text-to-model' });
|
|
// 定价段里没有这个版本 → 不可提交,不回落默认版本的价。
|
|
expect(
|
|
resolveModel3dGenerationQuote(
|
|
createDialog({ model3dModel: 'P1-20260311' }),
|
|
MULTI_VERSION_PRICING,
|
|
).ok,
|
|
).toBe(false);
|
|
});
|
|
|
|
it('v2.5 的贴图档位不带 textureQuality,其它版本照常给出', () => {
|
|
expect(
|
|
resolveModel3dGenerationParams({
|
|
model: 'v2.5-20250123',
|
|
tier: 'textured-standard',
|
|
quad: false,
|
|
smartLowPoly: false,
|
|
generateParts: false,
|
|
}),
|
|
).toMatchObject({
|
|
ok: true,
|
|
params: { texture: true, textureQuality: null },
|
|
});
|
|
expect(
|
|
resolveModel3dGenerationParams({
|
|
model: DEFAULT_MODEL3D_MODEL_VERSION,
|
|
tier: 'textured-standard',
|
|
quad: false,
|
|
smartLowPoly: false,
|
|
generateParts: false,
|
|
}),
|
|
).toMatchObject({
|
|
ok: true,
|
|
params: { texture: true, textureQuality: 'standard' },
|
|
});
|
|
});
|
|
|
|
it('版本不支持的能力组合直接判非法,不留给服务端拒', () => {
|
|
expect(
|
|
resolveModel3dGenerationParams({
|
|
model: 'P1-20260311',
|
|
tier: 'textured-detailed',
|
|
quad: false,
|
|
smartLowPoly: false,
|
|
generateParts: false,
|
|
}),
|
|
).toEqual({ ok: false, reason: '高清几何只支持 v3.1 / v3.0' });
|
|
expect(
|
|
resolveModel3dGenerationParams({
|
|
model: 'P2-20260801',
|
|
tier: 'textured-standard',
|
|
quad: false,
|
|
smartLowPoly: true,
|
|
generateParts: false,
|
|
}),
|
|
).toEqual({ ok: false, reason: '智能低多边形只支持 v3.1 / v3.0' });
|
|
expect(
|
|
resolveModel3dGenerationParams({
|
|
model: 'v2.5-20250123',
|
|
tier: 'geometry-only',
|
|
quad: true,
|
|
smartLowPoly: false,
|
|
generateParts: false,
|
|
}),
|
|
).toEqual({ ok: false, reason: '方形布线只支持 v3.1 / v3.0 / P2' });
|
|
});
|
|
|
|
it('换版本时收敛档位与开关并换代次', () => {
|
|
const detailed = createDialog({
|
|
model3dTier: 'textured-detailed',
|
|
model3dQuad: true,
|
|
model3dSmartLowPoly: true,
|
|
});
|
|
const p2 = applyModel3dModelChange(detailed, 'P2-20260801');
|
|
expect(p2.model3dModel).toBe('P2-20260801');
|
|
// 高清贴图只 v3.x 有,降到标准贴图而不是直接掉到纯几何。
|
|
expect(p2.model3dTier).toBe('textured-standard');
|
|
expect(p2.model3dQuad).toBe(true);
|
|
expect(p2.model3dSmartLowPoly).toBe(false);
|
|
expect(p2.model3dAttemptNonce).not.toBe(detailed.model3dAttemptNonce);
|
|
|
|
const v25 = applyModel3dModelChange(detailed, 'v2.5-20250123');
|
|
expect(v25.model3dTier).toBe('textured-standard');
|
|
expect(v25.model3dQuad).toBe(false);
|
|
expect(v25.model3dSmartLowPoly).toBe(false);
|
|
expect(v25.model3dGenerateParts).toBe(false);
|
|
});
|
|
|
|
it('换版本清掉失败态,切档位也按当前版本收敛', () => {
|
|
const failed = createDialog({
|
|
status: 'failed',
|
|
errorMessage: '3D 模型生成失败,请稍后重试。',
|
|
model3dModel: 'P2-20260801',
|
|
});
|
|
expect(applyModel3dModelChange(failed, 'P2-20260801')).toMatchObject({
|
|
status: 'idle',
|
|
errorMessage: undefined,
|
|
});
|
|
// P2 没有高清贴图档位:切过去只能落在标准贴图。
|
|
expect(
|
|
applyModel3dTierChange(failed, 'textured-detailed').model3dTier,
|
|
).toBe('textured-standard');
|
|
// 纯几何 + 分件在 v3.x 上仍然成立。
|
|
expect(
|
|
applyModel3dTierChange(
|
|
createDialog({ model3dGenerateParts: true }),
|
|
'geometry-only',
|
|
).model3dGenerateParts,
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('resolveModel3dPrice', () => {
|
|
const paramsOf = (
|
|
tier: 'textured-standard' | 'textured-detailed' | 'geometry-only',
|
|
switches: {
|
|
quad?: boolean;
|
|
smartLowPoly?: boolean;
|
|
generateParts?: boolean;
|
|
} = {},
|
|
) => {
|
|
const result = resolveModel3dGenerationParams({
|
|
model: DEFAULT_MODEL3D_MODEL_VERSION,
|
|
tier,
|
|
quad: switches.quad ?? false,
|
|
smartLowPoly: switches.smartLowPoly ?? false,
|
|
generateParts: switches.generateParts ?? false,
|
|
});
|
|
if (!result.ok) {
|
|
throw new Error('档位组合必须合法');
|
|
}
|
|
return result.params;
|
|
};
|
|
|
|
it('底价按端点与是否有贴图取,add-on 逐项相加', () => {
|
|
expect(
|
|
resolveModel3dPrice({
|
|
pricing: PRICING,
|
|
endpoint: 'image-to-model',
|
|
modelVersion: 'v3.1-20260211',
|
|
params: paramsOf('textured-standard'),
|
|
}),
|
|
).toEqual({ ok: true, price: 250 });
|
|
expect(
|
|
resolveModel3dPrice({
|
|
pricing: PRICING,
|
|
endpoint: 'text-to-model',
|
|
modelVersion: 'v3.1-20260211',
|
|
params: paramsOf('geometry-only'),
|
|
}),
|
|
).toEqual({ ok: true, price: 100 });
|
|
});
|
|
|
|
it('缺段、缺底价、缺 add-on 一律不可提交,不按 0 放行', () => {
|
|
expect(
|
|
resolveModel3dPrice({
|
|
pricing: null,
|
|
endpoint: 'text-to-model',
|
|
params: paramsOf('textured-standard'),
|
|
modelVersion: 'v3.1-20260211',
|
|
}),
|
|
).toEqual({ ok: false, reason: MODEL3D_PRICING_UNAVAILABLE_REASON });
|
|
expect(
|
|
resolveModel3dPrice({
|
|
pricing: { ...PRICING, basePrices: null },
|
|
endpoint: 'text-to-model',
|
|
modelVersion: 'v3.1-20260211',
|
|
params: paramsOf('textured-standard'),
|
|
}).ok,
|
|
).toBe(false);
|
|
expect(
|
|
resolveModel3dPrice({
|
|
// 纯几何 + 分件只叠 generateParts 一个 add-on,缺的就是它。
|
|
pricing: { ...PRICING, addOnPrices: { quadMesh: 40 } },
|
|
endpoint: 'text-to-model',
|
|
modelVersion: 'v3.1-20260211',
|
|
params: paramsOf('geometry-only', { generateParts: true }),
|
|
}),
|
|
).toEqual({ ok: false, reason: '3D 模型定价缺少 generateParts 价格' });
|
|
expect(
|
|
resolveModel3dPrice({
|
|
pricing: PRICING,
|
|
endpoint: 'text-to-model',
|
|
modelVersion: 'v3.0-20250812',
|
|
params: paramsOf('textured-standard'),
|
|
}).ok,
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('resolveModel3dGenerationQuote', () => {
|
|
it('文生与图生各取自己的端点价格', () => {
|
|
expect(resolveModel3dGenerationQuote(createDialog(), PRICING)).toEqual({
|
|
ok: true,
|
|
price: 200,
|
|
endpoint: 'text-to-model',
|
|
});
|
|
expect(
|
|
resolveModel3dGenerationQuote(
|
|
createDialog({ mode: 'model3d-image-to-model' }),
|
|
PRICING,
|
|
),
|
|
).toEqual({ ok: true, price: 250, endpoint: 'image-to-model' });
|
|
});
|
|
|
|
it('定价段缺失时两个子项都拿不到价格', () => {
|
|
for (const mode of [
|
|
'model3d-text-to-model',
|
|
'model3d-image-to-model',
|
|
] as Model3dGenerationMode[]) {
|
|
expect(
|
|
resolveModel3dGenerationQuote(createDialog({ mode }), null),
|
|
).toEqual({ ok: false, reason: MODEL3D_PRICING_UNAVAILABLE_REASON });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('3D 生成面板的参数变更', () => {
|
|
it('换档位时贴图档位强制关掉分件,并换代次', () => {
|
|
const geometryDialog = createDialog({
|
|
model3dTier: 'geometry-only',
|
|
model3dGenerateParts: true,
|
|
});
|
|
const textured = applyModel3dTierChange(
|
|
geometryDialog,
|
|
'textured-detailed',
|
|
);
|
|
expect(textured.model3dGenerateParts).toBe(false);
|
|
expect(textured.model3dAttemptNonce).not.toBe(
|
|
geometryDialog.model3dAttemptNonce,
|
|
);
|
|
|
|
const back = applyModel3dTierChange(textured, 'geometry-only');
|
|
expect(back.model3dTier).toBe('geometry-only');
|
|
expect(back.model3dAttemptNonce).not.toBe(textured.model3dAttemptNonce);
|
|
});
|
|
|
|
it('改开关、改提示词、主动重试都会换代次,并清掉失败态', () => {
|
|
const failedDialog = createDialog({
|
|
status: 'failed',
|
|
errorMessage: '3D 模型生成失败,请稍后重试。',
|
|
});
|
|
expect(
|
|
applyModel3dSwitchChange(failedDialog, { model3dQuad: true }),
|
|
).toMatchObject({ model3dQuad: true, status: 'idle' });
|
|
expect(
|
|
applyModel3dSwitchChange(failedDialog, { model3dQuad: true })
|
|
.model3dAttemptNonce,
|
|
).not.toBe(failedDialog.model3dAttemptNonce);
|
|
expect(applyModel3dPromptChange(failedDialog, '一把木椅')).toMatchObject({
|
|
prompt: '一把木椅',
|
|
status: 'idle',
|
|
errorMessage: undefined,
|
|
});
|
|
expect(
|
|
applyModel3dPromptChange(failedDialog, '一把木椅').model3dAttemptNonce,
|
|
).not.toBe(failedDialog.model3dAttemptNonce);
|
|
expect(refreshModel3dAttemptNonce(failedDialog)).toMatchObject({
|
|
status: 'idle',
|
|
errorMessage: undefined,
|
|
});
|
|
expect(
|
|
refreshModel3dAttemptNonce(failedDialog).model3dAttemptNonce,
|
|
).not.toBe(failedDialog.model3dAttemptNonce);
|
|
});
|
|
|
|
it('删掉参考图回到空槽位并清掉失败态', () => {
|
|
const dialog = createDialog({
|
|
mode: 'model3d-image-to-model',
|
|
status: 'failed',
|
|
errorMessage: '3D 模型生成失败,请稍后重试。',
|
|
generationReferences: [
|
|
{ id: 'reference-1', label: '参考图', src: 'data:image/png;base64,1' },
|
|
{ id: 'reference-2', label: '参考图', src: 'data:image/png;base64,2' },
|
|
],
|
|
});
|
|
|
|
const next = applyModel3dReferenceRemoval(dialog, 'reference-1');
|
|
|
|
expect(next.generationReferences?.map((reference) => reference.id)).toEqual(
|
|
['reference-2'],
|
|
);
|
|
expect(next).toMatchObject({ status: 'idle', errorMessage: undefined });
|
|
expect(
|
|
applyModel3dReferenceRemoval(next, 'reference-2').generationReferences,
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('每次铸造的代次互不相同', () => {
|
|
const nonces = new Set(
|
|
Array.from({ length: 20 }, () => mintModel3dAttemptNonce()),
|
|
);
|
|
expect(nonces.size).toBe(20);
|
|
for (const nonce of nonces) {
|
|
expect(nonce).toMatch(/^model3d-[0-9a-zA-Z-]+$/);
|
|
}
|
|
});
|
|
});
|