fixes
This commit is contained in:
+58
-13
@@ -149,6 +149,37 @@
|
|||||||
setProjectStringMap(ACTOR_NAMES_PROPERTY, names);
|
setProjectStringMap(ACTOR_NAMES_PROPERTY, names);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function remapActorReferences(previousNames, nextNames, indices, metadata) {
|
||||||
|
const labels = new Map(indices.map((index) => [previousNames[index] || `actor${index}`,
|
||||||
|
nextNames[index] || `actor${index}`]));
|
||||||
|
const nextLabels = [...labels.values()];
|
||||||
|
if (new Set(nextLabels).size !== nextLabels.length) throw new Error('Actor export names must be unique.');
|
||||||
|
const rename = (label) => labels.get(label) ?? label;
|
||||||
|
const targets = (value) => Array.isArray(value) ? value.map(rename) : typeof value === 'string' ? rename(value) : value;
|
||||||
|
for (const entry of Object.values(metadata)) {
|
||||||
|
if (entry.injects_into) entry.injects_into = targets(entry.injects_into);
|
||||||
|
}
|
||||||
|
const definitions = JSON.parse(Project[ANIMDEF_DEFINITIONS_PROPERTY] || '{}');
|
||||||
|
for (const definition of Object.values(definitions)) {
|
||||||
|
for (const actor of definition.actors || []) {
|
||||||
|
actor.label = rename(actor.label);
|
||||||
|
if (actor.injects_into) actor.injects_into = targets(actor.injects_into);
|
||||||
|
}
|
||||||
|
if (definition.position_anchor_actor) definition.position_anchor_actor = rename(definition.position_anchor_actor);
|
||||||
|
for (const stage of definition.stages || []) {
|
||||||
|
if (stage.props && typeof stage.props === 'object') {
|
||||||
|
stage.props = Object.fromEntries(Object.entries(stage.props).map(([label, value]) => [rename(label), value]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const previews = Object.fromEntries(Object.values(readPropPreviews()).map((entry) => {
|
||||||
|
const updated = { ...entry, actor: rename(entry.actor) };
|
||||||
|
return [propEntryKey(updated.animation, updated.actor, updated.slot, updated.stage), updated];
|
||||||
|
}));
|
||||||
|
Project[ANIMDEF_DEFINITIONS_PROPERTY] = JSON.stringify(definitions);
|
||||||
|
Project[PROP_PREVIEWS_PROPERTY] = JSON.stringify(previews);
|
||||||
|
}
|
||||||
|
|
||||||
function getProjectActorTextures() {
|
function getProjectActorTextures() {
|
||||||
return getProjectStringMap(ACTOR_TEXTURES_PROPERTY);
|
return getProjectStringMap(ACTOR_TEXTURES_PROPERTY);
|
||||||
}
|
}
|
||||||
@@ -392,6 +423,9 @@
|
|||||||
for (const label of actorLabels) {
|
for (const label of actorLabels) {
|
||||||
if (label !== actorLabel) targetOptions[label] = label;
|
if (label !== actorLabel) targetOptions[label] = label;
|
||||||
}
|
}
|
||||||
|
for (const label of metadata.injects_into || []) {
|
||||||
|
if (!Object.hasOwn(targetOptions, label)) targetOptions[label] = `${label} (unavailable - remove or replace)`;
|
||||||
|
}
|
||||||
form[`${base}_heading`] = {
|
form[`${base}_heading`] = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
text: `#### Actor ${actorIndex} (${actorLabel})`,
|
text: `#### Actor ${actorIndex} (${actorLabel})`,
|
||||||
@@ -1732,12 +1766,14 @@
|
|||||||
const aspects = { elements: Cube.all.slice(), uv_mode: true, actor_preferences_edit: true };
|
const aspects = { elements: Cube.all.slice(), uv_mode: true, actor_preferences_edit: true };
|
||||||
Undo.initEdit(aspects);
|
Undo.initEdit(aspects);
|
||||||
editing = true;
|
editing = true;
|
||||||
|
remapActorReferences(names, outNames, actorIndices, outAnimdefMetadata);
|
||||||
setProjectActorNames(outNames);
|
setProjectActorNames(outNames);
|
||||||
setProjectActorTextures(outTextures);
|
setProjectActorTextures(outTextures);
|
||||||
setProjectActorAnimdefMetadata(outAnimdefMetadata);
|
setProjectActorAnimdefMetadata(outAnimdefMetadata);
|
||||||
setProjectBoneTextureOverrides(outBoneTextureOverrides);
|
setProjectBoneTextureOverrides(outBoneTextureOverrides);
|
||||||
applyAllActorTexturePreviews(true);
|
applyAllActorTexturePreviews(true);
|
||||||
Undo.finishEdit('Edit Actor Preferences', { ...aspects, elements: Cube.all.slice() });
|
Undo.finishEdit('Edit Actor Preferences', { ...aspects, elements: Cube.all.slice() });
|
||||||
|
void refreshPropPreviews(true);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (editing) Undo.cancelEdit();
|
if (editing) Undo.cancelEdit();
|
||||||
@@ -3371,9 +3407,9 @@
|
|||||||
const cycleSeconds = parseRequiredPositiveNumber(result?.[`${base}_cycle_seconds`], `Stage p${stage.stageNumber} cycle seconds`);
|
const cycleSeconds = parseRequiredPositiveNumber(result?.[`${base}_cycle_seconds`], `Stage p${stage.stageNumber} cycle seconds`);
|
||||||
const speed = parseOptionalNumber(result?.[`${base}_speed`], `Stage p${stage.stageNumber} speed`, (value) => value > 0);
|
const speed = parseOptionalNumber(result?.[`${base}_speed`], `Stage p${stage.stageNumber} speed`, (value) => value > 0);
|
||||||
const midpointOffset = parseOptionalNumber(
|
const midpointOffset = parseOptionalNumber(
|
||||||
result?.[`${base}_midpoint_offset`],
|
loop ? result?.[`${base}_midpoint_offset`] : '',
|
||||||
`Stage p${stage.stageNumber} midpoint offset`,
|
`Stage p${stage.stageNumber} midpoint offset`,
|
||||||
() => loop
|
() => true
|
||||||
);
|
);
|
||||||
const stageSeconds = parseOptionalNumber(
|
const stageSeconds = parseOptionalNumber(
|
||||||
result?.[`${base}_stage_seconds`],
|
result?.[`${base}_stage_seconds`],
|
||||||
@@ -3381,12 +3417,12 @@
|
|||||||
(value) => Number.isInteger(value) && (value === -1 || (value >= 1 && value <= 300))
|
(value) => Number.isInteger(value) && (value === -1 || (value >= 1 && value <= 300))
|
||||||
);
|
);
|
||||||
const durationMultiplier = parseOptionalNumber(
|
const durationMultiplier = parseOptionalNumber(
|
||||||
result?.[`${base}_duration_multiplier`],
|
loop ? result?.[`${base}_duration_multiplier`] : '',
|
||||||
`Stage p${stage.stageNumber} duration multiplier`,
|
`Stage p${stage.stageNumber} duration multiplier`,
|
||||||
(value) => loop && value >= 0.05 && value <= 20
|
(value) => value >= 0.05 && value <= 20
|
||||||
);
|
);
|
||||||
const useStage = parseOptionalNumber(
|
const useStage = parseOptionalNumber(
|
||||||
result?.[`${base}_use_stage`],
|
result?.[`${base}_use_stage`] === 'own' ? '' : result?.[`${base}_use_stage`],
|
||||||
`Stage p${stage.stageNumber} playback stage`,
|
`Stage p${stage.stageNumber} playback stage`,
|
||||||
(value) => Number.isInteger(value) && value > 0
|
(value) => Number.isInteger(value) && value > 0
|
||||||
);
|
);
|
||||||
@@ -3403,13 +3439,13 @@
|
|||||||
? !!result[`${base}_allow_join`]
|
? !!result[`${base}_allow_join`]
|
||||||
: !isLastStage,
|
: !isLastStage,
|
||||||
};
|
};
|
||||||
if (midpointOffset !== null) output.cycle_midpoint_offset_seconds = midpointOffset;
|
if (loop && midpointOffset !== null) output.cycle_midpoint_offset_seconds = midpointOffset;
|
||||||
if (useStage !== null && useStage !== stage.stageNumber) output.use_stage = useStage;
|
if (useStage !== null && useStage !== stage.stageNumber) output.use_stage = useStage;
|
||||||
if (speed !== null) output.speed = speed;
|
if (speed !== null) output.speed = speed;
|
||||||
if (peaked) output.non_peak = true;
|
if (peaked) output.non_peak = true;
|
||||||
if (escapable !== 'default') output.escapable = escapable === 'true';
|
if (escapable !== 'default') output.escapable = escapable === 'true';
|
||||||
if (stageSeconds !== null) output.stage_seconds = stageSeconds;
|
if (stageSeconds !== null) output.stage_seconds = stageSeconds;
|
||||||
if (durationMultiplier !== null) output.stage_duration_multiplier = durationMultiplier;
|
if (loop && durationMultiplier !== null) output.stage_duration_multiplier = durationMultiplier;
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3829,6 +3865,9 @@
|
|||||||
const label = analysis.actorLabels.get(actorIndex);
|
const label = analysis.actorLabels.get(actorIndex);
|
||||||
anchorOptions[label] = label;
|
anchorOptions[label] = label;
|
||||||
}
|
}
|
||||||
|
if (saved.position_anchor_actor && !Object.hasOwn(anchorOptions, saved.position_anchor_actor)) {
|
||||||
|
anchorOptions[saved.position_anchor_actor] = `${saved.position_anchor_actor} (unavailable - replace)`;
|
||||||
|
}
|
||||||
form.position_anchor_actor = {
|
form.position_anchor_actor = {
|
||||||
label: 'Position anchor actor', type: 'select', value: saved.position_anchor_actor || '', options: anchorOptions,
|
label: 'Position anchor actor', type: 'select', value: saved.position_anchor_actor || '', options: anchorOptions,
|
||||||
description: 'Prefer this actor as the animation placement anchor.',
|
description: 'Prefer this actor as the animation placement anchor.',
|
||||||
@@ -3929,15 +3968,18 @@
|
|||||||
label: 'Play animation from stage',
|
label: 'Play animation from stage',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: Object.fromEntries([
|
options: Object.fromEntries([
|
||||||
['', `Own stage (p${stage.stageNumber})`],
|
['own', `Own stage (p${stage.stageNumber})`],
|
||||||
...analysis.stages.map((availableStage) => [
|
...analysis.stages.map((availableStage) => [
|
||||||
String(availableStage.stageNumber), `Stage p${availableStage.stageNumber}`,
|
String(availableStage.stageNumber), `Stage p${availableStage.stageNumber}`,
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
value: savedStage.use_stage == null ? '' : String(savedStage.use_stage),
|
value: savedStage.use_stage == null ? 'own' : String(savedStage.use_stage),
|
||||||
description: 'Optional stage number whose GeckoLib clip should be played instead.',
|
description: 'Optional stage number whose GeckoLib clip should be played instead.',
|
||||||
condition: showAdvanced,
|
condition: showAdvanced,
|
||||||
};
|
};
|
||||||
|
if (savedStage.use_stage != null && !Object.hasOwn(form[`${base}_use_stage`].options, String(savedStage.use_stage))) {
|
||||||
|
form[`${base}_use_stage`].options[String(savedStage.use_stage)] = `Stage p${savedStage.use_stage} (unavailable - replace)`;
|
||||||
|
}
|
||||||
form[`${base}_stage_seconds`] = {
|
form[`${base}_stage_seconds`] = {
|
||||||
label: 'Stage seconds',
|
label: 'Stage seconds',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -4041,6 +4083,7 @@
|
|||||||
const water = String(result?.water || 'none');
|
const water = String(result?.water || 'none');
|
||||||
if (water !== 'none') definition.water = water;
|
if (water !== 'none') definition.water = water;
|
||||||
const anchor = String(result?.position_anchor_actor || '');
|
const anchor = String(result?.position_anchor_actor || '');
|
||||||
|
if (anchor && !actorsByLabel.has(anchor)) throw new Error(`Position anchor "${anchor}" is unavailable. Choose another actor or Automatic anchor.`);
|
||||||
if (anchor) definition.position_anchor_actor = anchor;
|
if (anchor) definition.position_anchor_actor = anchor;
|
||||||
const camera = unmanagedAnimdefFields(saved.camera, new Set(['torso_tracking', 'block_requirement_collision']));
|
const camera = unmanagedAnimdefFields(saved.camera, new Set(['torso_tracking', 'block_requirement_collision']));
|
||||||
if (result?.camera_torso_tracking === 'continuous') camera.torso_tracking = 'continuous';
|
if (result?.camera_torso_tracking === 'continuous') camera.torso_tracking = 'continuous';
|
||||||
@@ -6656,7 +6699,7 @@
|
|||||||
Plugin.register(PLUGIN_ID, {
|
Plugin.register(PLUGIN_ID, {
|
||||||
title: 'AnimationDirector',
|
title: 'AnimationDirector',
|
||||||
author: 'L1Z0',
|
author: 'L1Z0',
|
||||||
icon: 'cinematic_blur',
|
icon: 'diversity_4',
|
||||||
description: 'Multi-actor animation in Blockbench without bone-name clashes.',
|
description: 'Multi-actor animation in Blockbench without bone-name clashes.',
|
||||||
about: `AnimationDirector is a Blockbench plugin for creating multi-actor animations for Needs of Nature.
|
about: `AnimationDirector is a Blockbench plugin for creating multi-actor animations for Needs of Nature.
|
||||||
|
|
||||||
@@ -6671,6 +6714,7 @@ Build, preview, and export multi-actor animations with tools for actor models, a
|
|||||||
- Edit block requirements with live previews of required blocks and clearance areas.
|
- Edit block requirements with live previews of required blocks and clearance areas.
|
||||||
- Preview item props using Minecraft assets or custom textures. <sup>1</sup>
|
- Preview item props using Minecraft assets or custom textures. <sup>1</sup>
|
||||||
- Browse and audition sounds with the NoN Sound Library.
|
- Browse and audition sounds with the NoN Sound Library.
|
||||||
|
- Browse particle sprites and assign cues and locators with the NoN Particle Library, including approximate timeline previews for supported NoN particles. <sup>1</sup>
|
||||||
- Export multi-actor, multi-stage animations and GeckoLib models.
|
- Export multi-actor, multi-stage animations and GeckoLib models.
|
||||||
- Replace actor models while preserving matching animation tracks.
|
- Replace actor models while preserving matching animation tracks.
|
||||||
- Transfer added geometry into vanilla CEM presets to create JEM models, with optional energy-dependent visibility. <sup>2</sup>
|
- Transfer added geometry into vanilla CEM presets to create JEM models, with optional energy-dependent visibility. <sup>2</sup>
|
||||||
@@ -6679,11 +6723,11 @@ Build, preview, and export multi-actor animations with tools for actor models, a
|
|||||||
|
|
||||||
**Addon Notes**
|
**Addon Notes**
|
||||||
|
|
||||||
<sup>1</sup> Asset Browser - optional; its cached Minecraft assets can be used for textures and item previews.
|
<sup>1</sup> Asset Browser - optional; its cached Minecraft assets can be used for textures, item previews, and the particle library.
|
||||||
|
|
||||||
<sup>2</sup> CEM Template Loader - required for JEM creation.
|
<sup>2</sup> CEM Template Loader - required for JEM creation.
|
||||||
|
|
||||||
Features without a reference need no additional addon. Whole-animation import and local asset tools require desktop Blockbench. The sound library uses locally available NoN/pack sound files.`,
|
Features without a reference need no additional addon. Whole-animation import and local asset tools require desktop Blockbench. The sound library uses locally available NoN/pack sound files; NoN particle previews require their texture assets from a NoN JAR or extracted assets folder.`,
|
||||||
version: '2.0',
|
version: '2.0',
|
||||||
min_version: '5.2.1',
|
min_version: '5.2.1',
|
||||||
variant: 'both',
|
variant: 'both',
|
||||||
@@ -6821,7 +6865,8 @@ Features without a reference need no additional addon. Whole-animation import an
|
|||||||
if (aspects.actor_texture_edit) save[ACTOR_TEXTURES_PROPERTY] = Project[ACTOR_TEXTURES_PROPERTY] || '{}';
|
if (aspects.actor_texture_edit) save[ACTOR_TEXTURES_PROPERTY] = Project[ACTOR_TEXTURES_PROPERTY] || '{}';
|
||||||
if (aspects.actor_preferences_edit) {
|
if (aspects.actor_preferences_edit) {
|
||||||
for (const property of [ACTOR_NAMES_PROPERTY, ACTOR_TEXTURES_PROPERTY, ACTOR_ANIMDEF_METADATA_PROPERTY,
|
for (const property of [ACTOR_NAMES_PROPERTY, ACTOR_TEXTURES_PROPERTY, ACTOR_ANIMDEF_METADATA_PROPERTY,
|
||||||
BONE_TEXTURE_OVERRIDES_PROPERTY, ACTOR_UV_BASIS_PROPERTY]) save[property] = Project[property] || '{}';
|
BONE_TEXTURE_OVERRIDES_PROPERTY, ACTOR_UV_BASIS_PROPERTY, ANIMDEF_DEFINITIONS_PROPERTY,
|
||||||
|
PROP_PREVIEWS_PROPERTY]) save[property] = Project[property] || '{}';
|
||||||
}
|
}
|
||||||
if (aspects.automatic_animation_import) {
|
if (aspects.automatic_animation_import) {
|
||||||
for (const property of IMPORT_METADATA) save[property] = Project[property] ?? (property === ANIMDEF_ANIMATORS_PROPERTY ? '' : '{}');
|
for (const property of IMPORT_METADATA) save[property] = Project[property] ?? (property === ANIMDEF_ANIMATORS_PROPERTY ? '' : '{}');
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function createHarness() {
|
|||||||
const file = path.join(__dirname, '..', 'AnimationDirector.js');
|
const file = path.join(__dirname, '..', 'AnimationDirector.js');
|
||||||
const source = fs.readFileSync(file, 'utf8').replace(
|
const source = fs.readFileSync(file, 'utf8').replace(
|
||||||
' Plugin.register(PLUGIN_ID, {',
|
' Plugin.register(PLUGIN_ID, {',
|
||||||
' globalThis.adTest = { showAnimationDefinitionReviewDialog, createInjectorGenderDefaultHandler };\n Plugin.register(PLUGIN_ID, {'
|
' globalThis.adTest = { showAnimationDefinitionReviewDialog, createInjectorGenderDefaultHandler, remapActorReferences, readStageDefinitionFromForm };\n Plugin.register(PLUGIN_ID, {'
|
||||||
);
|
);
|
||||||
const dialogs = [];
|
const dialogs = [];
|
||||||
const exports = [];
|
const exports = [];
|
||||||
@@ -45,7 +45,7 @@ function createHarness() {
|
|||||||
context.adTest.showAnimationDefinitionReviewDialog(group, analysis);
|
context.adTest.showAnimationDefinitionReviewDialog(group, analysis);
|
||||||
return dialogs.at(-1);
|
return dialogs.at(-1);
|
||||||
}
|
}
|
||||||
return { open, exports, errors, project, analysis, createInjectorGenderDefaultHandler: context.adTest.createInjectorGenderDefaultHandler,
|
return { open, exports, errors, project, analysis, helpers: context.adTest, createInjectorGenderDefaultHandler: context.adTest.createInjectorGenderDefaultHandler,
|
||||||
setFormResult(result) { formResult = result; } };
|
setFormResult(result) { formResult = result; } };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +55,50 @@ function defaults(form) {
|
|||||||
.map(([key, field]) => [key, field.value]));
|
.map(([key, field]) => [key, field.value]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('actor renames remap all definitions, metadata and prop previews simultaneously', () => {
|
||||||
|
const h = createHarness();
|
||||||
|
const metadata = { 1: { injects_into: ['wolf'] } };
|
||||||
|
const definition = { actors: [{ label: 'player', injects_into: 'wolf', prop_right: 'minecraft:stick' },
|
||||||
|
{ label: 'wolf' }], position_anchor_actor: 'wolf', stages: [{ stage: 1, props: { wolf: null, player: { prop_left: null } } }] };
|
||||||
|
h.project.animationdirector_animdef_definitions = JSON.stringify({ first: definition, second: definition });
|
||||||
|
h.project.animationdirector_prop_previews = JSON.stringify({ old: { animation: 'first', actor: 'wolf', slot: 'right', stage: 1 } });
|
||||||
|
h.helpers.remapActorReferences({ 1: 'player', 2: 'wolf' }, { 1: 'wolf', 2: 'player' }, [1, 2], metadata);
|
||||||
|
assert.deepEqual(metadata[1].injects_into, ['player']);
|
||||||
|
for (const saved of Object.values(JSON.parse(h.project.animationdirector_animdef_definitions))) {
|
||||||
|
assert.equal(saved.actors[0].label, 'wolf');
|
||||||
|
assert.equal(saved.actors[0].injects_into, 'player');
|
||||||
|
assert.equal(saved.actors[0].prop_right, 'minecraft:stick');
|
||||||
|
assert.equal(saved.position_anchor_actor, 'player');
|
||||||
|
assert.equal(saved.stages[0].props.player, null);
|
||||||
|
assert.deepEqual(saved.stages[0].props.wolf, { prop_left: null });
|
||||||
|
}
|
||||||
|
const previews = JSON.parse(h.project.animationdirector_prop_previews);
|
||||||
|
assert.equal(previews[JSON.stringify(['first', 'player', 'right', 1])].actor, 'player');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('own stage remains explicit and hidden loop controls do not block non-looping stages', () => {
|
||||||
|
const h = createHarness(), dialog = h.open(), result = defaults(dialog.form);
|
||||||
|
assert.equal(result.stub_stage_2_use_stage, 'own');
|
||||||
|
result.stub_stage_2_midpoint_offset = 'not applicable';
|
||||||
|
result.stub_stage_2_duration_multiplier = 'not applicable';
|
||||||
|
const stage = h.helpers.readStageDefinitionFromForm(result, h.analysis.stages[1], true);
|
||||||
|
assert.equal(stage.use_stage, undefined);
|
||||||
|
assert.equal(stage.cycle_midpoint_offset_seconds, undefined);
|
||||||
|
assert.equal(stage.stage_duration_multiplier, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('obsolete injection targets and position anchors remain visible for correction', () => {
|
||||||
|
const h = createHarness();
|
||||||
|
h.project.animationdirector_animdef_definitions = JSON.stringify({ test_animation: {
|
||||||
|
actors: [{ label: 'player', entity_types: ['minecraft:player'], injector: 'V', injects_into: 'removed' }],
|
||||||
|
position_anchor_actor: 'removed', stages: [{ stage: 2, use_stage: 99 }],
|
||||||
|
} });
|
||||||
|
const dialog = h.open();
|
||||||
|
assert.match(dialog.form.stub_actor_1_injects_into.options.removed, /unavailable/);
|
||||||
|
assert.match(dialog.form.position_anchor_actor.options.removed, /unavailable/);
|
||||||
|
assert.match(dialog.form.stub_stage_2_use_stage.options['99'], /unavailable/);
|
||||||
|
});
|
||||||
|
|
||||||
test('selecting an injector adds an editable male tag without enforcing it on export', () => {
|
test('selecting an injector adds an editable male tag without enforcing it on export', () => {
|
||||||
for (const role of ['V', 'A', 'M']) {
|
for (const role of ['V', 'A', 'M']) {
|
||||||
const h = createHarness(), dialog = h.open(), result = defaults(dialog.form), updates = [];
|
const h = createHarness(), dialog = h.open(), result = defaults(dialog.form), updates = [];
|
||||||
@@ -265,7 +309,7 @@ test('groups advanced matching and offers only other actors as injection targets
|
|||||||
'', 'manualpeak', 'defeated', 'stuck', 'tease', 'birth', 'fillbottle', 'other',
|
'', 'manualpeak', 'defeated', 'stuck', 'tease', 'birth', 'fillbottle', 'other',
|
||||||
]);
|
]);
|
||||||
assert.equal(form.stub_stage_2_use_stage.type, 'select');
|
assert.equal(form.stub_stage_2_use_stage.type, 'select');
|
||||||
assert.deepEqual(Object.keys(form.stub_stage_2_use_stage.options), ['1', '2', '']);
|
assert.deepEqual(Object.keys(form.stub_stage_2_use_stage.options), ['1', '2', 'own']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keeps minimal definitions minimal and supports specialized behavior', () => {
|
test('keeps minimal definitions minimal and supports specialized behavior', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user