85 lines
3.9 KiB
JavaScript
85 lines
3.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readdirSync, readFileSync } from "node:fs";
|
|
import { join, relative } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import test from "node:test";
|
|
import { compileScript, compileTemplate, parse } from "vue/compiler-sfc";
|
|
import ts from "typescript";
|
|
|
|
const root = fileURLToPath(new URL("../src/", import.meta.url));
|
|
const nativeControls = new Set([
|
|
"switch", "slider", "picker", "picker-view", "checkbox", "checkbox-group",
|
|
"radio", "radio-group",
|
|
]);
|
|
|
|
function vueFiles(directory) {
|
|
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
const path = join(directory, entry.name);
|
|
return entry.isDirectory() ? vueFiles(path) : path.endsWith(".vue") ? [path] : [];
|
|
});
|
|
}
|
|
|
|
function compile(source, filename) {
|
|
return compileTemplate({
|
|
source, filename, id: "native-component-regression",
|
|
compilerOptions: {
|
|
isNativeTag: (tag) => nativeControls.has(tag) || ["view", "text", "input", "textarea"].includes(tag),
|
|
},
|
|
});
|
|
}
|
|
|
|
function elements(node) {
|
|
return [node, ...(node.children ?? []).flatMap(elements)].filter((item) => item.type === 1);
|
|
}
|
|
|
|
test("the check detects unsupported v-model on native switches", () => {
|
|
const result = compile('<switch v-model="enabled" />', "invalid-switch.vue");
|
|
assert.ok(result.errors.some((error) => String(error.message).includes("v-model can only be used")));
|
|
});
|
|
|
|
test("all mobile templates compile without unsupported native v-model", () => {
|
|
const files = vueFiles(root);
|
|
assert.ok(files.length > 0);
|
|
for (const filename of files) {
|
|
const { descriptor, errors } = parse(readFileSync(filename, "utf8"), { filename });
|
|
assert.deepEqual(errors, [], relative(root, filename));
|
|
if (!descriptor.template) continue;
|
|
const result = compile(descriptor.template.content, filename);
|
|
assert.deepEqual(result.errors, [], relative(root, filename));
|
|
}
|
|
});
|
|
|
|
for (const [page, model] of [["settings", "privacy"], ["notification-settings", "form"]]) {
|
|
test(`${page} switch change updates both checked and unchecked form values`, () => {
|
|
const filename = join(root, "pages", page, "index.vue");
|
|
const { descriptor } = parse(readFileSync(filename, "utf8"), { filename });
|
|
const switches = elements(descriptor.template.ast).filter((node) => node.tag === "switch");
|
|
assert.equal(switches.length, 1);
|
|
const props = switches[0].props;
|
|
assert.ok(!props.some((prop) => prop.name === "model"));
|
|
const checked = props.find((prop) => prop.name === "bind" && prop.arg?.content === "checked");
|
|
const change = props.find((prop) => prop.name === "on" && prop.arg?.content === "change");
|
|
assert.ok(checked?.exp?.content);
|
|
assert.ok(change?.exp?.content);
|
|
const state = { enabled: false, otherSetting: true };
|
|
// Exercise the actual event handler and bindings without mounting a native
|
|
// runtime or calling the API; only the component's own handler is evaluated.
|
|
const script = compileScript(descriptor, { id: `test-${page}` });
|
|
const handler = script.scriptSetupAst.find((node) => node.type === "FunctionDeclaration" && node.id.name === "setSwitch");
|
|
assert.ok(handler);
|
|
const handlerSource = descriptor.scriptSetup.content.slice(handler.start, handler.end);
|
|
const handlerJS = ts.transpileModule(handlerSource, {
|
|
compilerOptions: { target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const setSwitch = new Function(model, `${handlerJS}; return setSwitch;`)({ value: state });
|
|
const update = new Function("setSwitch", "item", "$event", change.exp.content);
|
|
const readChecked = new Function(model, "item", `return ${checked.exp.content}`);
|
|
for (const value of [true, false, true]) {
|
|
update(setSwitch, ["enabled"], { detail: { value } });
|
|
assert.equal(state.enabled, value);
|
|
assert.equal(readChecked(state, ["enabled"]), value);
|
|
assert.equal(state.otherSetting, true);
|
|
}
|
|
});
|
|
}
|