Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247import { describe, expect, it } from "vitest";import { parseScript, type Command, type ParseError, type Script } from "../../runner/parser";
const wrap = (...commands: string[]) => `test "t"\nstep "s"\n${commands.map((c) => " " + c).join("\n")}`;const ok = (text: string): Script => { const r = parseScript(text); if (!r.ok) throw new Error("expected success, got " + JSON.stringify(r.errors)); return r.script;};const one = (command: string): Command => ok(wrap(command)).steps[0]!.commands[0]!;const errors = (text: string): ParseError[] => { const r = parseScript(text); if (r.ok) throw new Error("expected errors"); return r.errors;};
describe("every verb parses to the documented fields", () => { it.each<[string, object]>([ ["start https://a.test/x", { verb: "start", url: "https://a.test/x" }], ["goto https://a.test/y?q=1", { verb: "goto", url: "https://a.test/y?q=1" }], ['click text:"Go"', { verb: "click", target: { text: "Go" } }], ['doubleClick css:".row"', { verb: "doubleClick", target: { css: ".row" } }], ['check label:"Agree"', { verb: "check", target: { label: "Agree" } }], ['uncheck label:"Agree"', { verb: "uncheck", target: { label: "Agree" } }], ['type label:"Amount" "1"', { verb: "type", target: { label: "Amount" }, text: "1" }], ['type placeholder:"Search" ""', { verb: "type", target: { placeholder: "Search" }, text: "" }], ['select label:"Color" "Dark blue"', { verb: "select", target: { label: "Color" }, option: "Dark blue" }], ["press Enter", { verb: "press", key: "Enter" }], ["sleep 2s", { verb: "sleep", ms: 2000 }], ["sleep 250ms", { verb: "sleep", ms: 250 }], ["sleep 1m", { verb: "sleep", ms: 60000 }], ['capture before css:".balance"', { verb: "capture", name: "before", target: { css: ".balance" } }], ['set total "5"', { verb: "set", name: "total", value: "5" }], ['switchTab url~:"/pay"', { verb: "switchTab", tab: { kind: "url", contains: "/pay" } }], ["switchTab new", { verb: "switchTab", tab: { kind: "new" } }], ["closeTab", { verb: "closeTab" }], ['drag text:"Card" to:text:"Done"', { verb: "drag", target: { text: "Card" }, to: { text: "Done" } }], ['screenshot "after login"', { verb: "screenshot", name: "after login" }], ['call balanceMatches 3 "a b" x', { verb: "call", name: "balanceMatches", args: ["3", "a b", "x"] }], ["call noArgs", { verb: "call", name: "noArgs", args: [] }], ["run shared/login.aite", { verb: "run", path: "shared/login.aite" }], ])("%s", (text, expected) => { expect(one(text)).toMatchObject({ ...expected, line: 3 }); });});
describe("verify and verifyNot forms", () => { it.each<[string, object]>([ ['verify css:".balance"', { kind: "exists", target: { css: ".balance" } }], ['verify text:"1 MALLBUCKS"', { kind: "text", value: "1 MALLBUCKS" }], ['verify css:".b" text:"1 MALLBUCKS"', { kind: "targetText", target: { css: ".b" }, value: "1 MALLBUCKS" }], ['verify label:"Name" value:"alex"', { kind: "value", target: { label: "Name" }, value: "alex" }], ['verify role:button name:"Go" enabled', { kind: "state", target: { role: "button", name: "Go" }, state: "enabled" }], ['verify role:button name:"Go" disabled', { kind: "state", state: "disabled" }], ['verify label:"Agree" checked', { kind: "state", state: "checked" }], ['verify css:".row" count:3', { kind: "count", target: { css: ".row" }, count: 3 }], ['verify css:".row" count:0', { kind: "count", count: 0 }], ['verify url~:"/gift/"', { kind: "url", contains: "/gift/" }], ['verify title:"Mallbucks"', { kind: "title", value: "Mallbucks" }], ['verify css:".balance" number:changed-by from:before equals:-1', { kind: "numberChange", target: { css: ".balance" }, from: "before", equals: -1 }], ['verify css:".b" number:changed-by from:before equals:2.5', { kind: "numberChange", equals: 2.5 }], ['verify text:"x" within:"Review send"', { kind: "text", within: "Review send" }], ['verify text:"x" within:"Box" timeout:5s', { kind: "text", within: "Box", timeoutMs: 5000 }], ])("verify: %s", (text, expected) => { expect(one(text)).toMatchObject({ verb: "verify", ...expected }); expect(one(text.replace("verify", "verifyNot"))).toMatchObject({ verb: "verifyNot", ...expected }); }); it("a text key beside other keys is the expected text, and is not part of the target", () => { expect(one('verify css:".b" text:"x"')).not.toHaveProperty("target.text"); });});
describe("wait forms", () => { it.each<[string, object]>([ ['wait text:"Saved"', { kind: "visible", target: { text: "Saved" } }], ['wait css:".spin" gone', { kind: "gone", target: { css: ".spin" } }], ['wait css:".n" text-changes', { kind: "textChanges", target: { css: ".n" } }], ['wait url~:"/claim"', { kind: "url", contains: "/claim" }], ["wait network-idle timeout:10s", { kind: "networkIdle", timeoutMs: 10000 }], ['wait text:"x" within:"Panel"', { kind: "visible", within: "Panel" }], ])("%s", (text, expected) => { expect(one(text)).toMatchObject({ verb: "wait", ...expected }); });});
describe("modifiers", () => { it("as: is stored on any command", () => { expect(one('capture x css:".a" as:balance')).toMatchObject({ as: "balance" }); expect(one('click text:"Go" as:go')).toMatchObject({ as: "go" }); }); it("timeout: is stored in ms on any command", () => { expect(one("sleep 1s timeout:2s")).toMatchObject({ timeoutMs: 2000 }); });});
describe("start as a command inside a step", () => { it("is a command, and the header form still sets the script's start", () => { const s = ok('test "t"\nstart http://a.test/\nstep "s"\n start http://b.test/'); expect(s.start).toBe("http://a.test/"); expect(s.steps[0]!.commands[0]).toMatchObject({ verb: "start", url: "http://b.test/" }); });});
describe("E003: a command is missing what it needs", () => { it.each<[string, string]>([ ["type label:\"A\"", 'type needs text after the target, for example type label:"Amount" "1"'], ['type "1"', 'type needs a target, for example type label:"Amount" "1"'], ['select label:"A"', 'select needs an option after the target, for example select label:"Color" "Blue"'], ["press", "press needs a key, for example press Enter"], ["sleep", "sleep needs a duration such as 2s"], ["sleep soon", "sleep needs a duration such as 2s"], ["capture before", 'capture needs a name and a target, for example capture before css:".balance"'], ['capture css:".b"', 'capture needs a name and a target, for example capture before css:".balance"'], ["set total", 'set needs a name and a value, for example set total "5"'], ["switchTab", 'switchTab needs url~:"/part" or new'], ['drag text:"A"', 'drag needs a target and to:, for example drag text:"Card" to:text:"Done"'], ["screenshot", 'screenshot needs a name, for example screenshot "after login"'], ["call", "call needs a check name, for example call balanceMatches"], ["run", "run needs a script path, for example run shared/login.aite"], ["goto", "goto needs a URL, for example goto https://example.com/"], ["goto /relative", "goto needs a URL, for example goto https://example.com/"], ["wait", 'wait needs something to wait for, for example wait text:"Saved"'], ['click index:0', "index needs a whole number from 1, for example index:2"], ['click text:"a" text:"b"', 'text is written twice in one target'], ['verify css:".r" count:many', "count needs a whole number, for example count:3"], ['verify css:".r" number:changed-by', "number:changed-by needs from:NAME and equals:NUMBER"], ['verify css:".r" enabled checked', "verify can check only one of enabled, disabled, checked"], ['verify url~:"/a" title:"b"', "this combination of verify is not in the script format"], ['sleep 1s within:"x"', "within: is only for verify and wait"], ])("%s", (command, message) => { expect(errors(wrap(command))[0]).toMatchObject({ code: "E003", line: 3, message }); });});
describe("E005 and E010 inside the new commands", () => { it("an unknown key in any command is E005", () => { expect(errors(wrap('type txt:"x" "1"'))[0]).toMatchObject({ code: "E005", line: 3 }); expect(errors(wrap('verify bogus:"x"'))[0]).toMatchObject({ code: "E005" }); }); it("a stray word is E010", () => { expect(errors(wrap('closeTab now'))[0]).toMatchObject({ code: "E010", message: 'unexpected "now"' }); expect(errors(wrap('click text:"x" fast'))[0]).toMatchObject({ code: "E010", message: 'unexpected "fast"' }); }); it("a bad duration is E010", () => { expect(errors(wrap('wait text:"x" timeout:soon'))[0]).toMatchObject({ code: "E010", message: "timeout needs a duration such as 10s, 500ms or 1m" }); });});
describe("E007: a variable used before it is set", () => { const e7 = (name: string) => `\${${name}} is used before it is set (capture or set it first)`; it("reports the first use, on the line of the string", () => { expect(errors('test "t"\nstep "s"\n type label:"A" "${total}"')[0]).toMatchObject({ code: "E007", line: 3, message: e7("total") }); }); it("is satisfied by an earlier set or capture, in this step or an earlier one", () => { expect(() => ok('test "t"\nstep "a"\n set total "5"\n type label:"A" "${total}"\nstep "b"\n capture n css:".n"\n verify text:"${n}"')).not.toThrow(); }); it("is not satisfied by a later definition", () => { expect(errors('test "t"\nstep "s"\n verify text:"${x}"\n set x "1"')[0]).toMatchObject({ code: "E007", line: 3 }); }); it("a set may not use itself", () => { expect(errors('test "t"\nstep "s"\n set x "${x}1"')[0]).toMatchObject({ code: "E007" }); }); it("applies to strings in targets, to screenshot names and to call args", () => { expect(errors(wrap('click text:"${a}"'))[0]!.code).toBe("E007"); expect(errors(wrap('screenshot "${a}"'))[0]!.code).toBe("E007"); expect(errors(wrap('call chk "${a}"'))[0]!.code).toBe("E007"); }); it("reports each unknown name once per line, in order", () => { expect(errors(wrap('type label:"${a}" "${b} ${a}"')).map((e) => e.message)).toEqual([e7("a"), e7("b")]); }); it("a dollar sign without braces is plain text", () => { expect(() => ok(wrap('verify text:"$5 and ${ not closed"'))).not.toThrow(); });});
describe("totality", () => { it("a URL as a bare value is not mistaken for a key", () => { expect(one('call chk https://a.test/x')).toMatchObject({ args: ["https://a.test/x"] }); }); it("only the first of two errors on one line is reported for that command", () => { expect(errors(wrap('click txt:"a" bogus:"b"')).length).toBeGreaterThan(0); });});
describe("parseScript options: variables already set, and the file a command came from", () => { const text = 'test "t"\nstep "s"\n verify text:"${who}"'; it("variables the caller says are set do not give E007", () => { expect(parseScript(text).ok).toBe(false); expect(parseScript(text, { variables: ["who"] }).ok).toBe(true); }); it("other names are still E007", () => { const r = parseScript('test "t"\nstep "s"\n verify text:"${other}"', { variables: ["who"] }); expect(r.ok).toBe(false); }); it("every command carries the file when one is given, and none otherwise", () => { const withFile = parseScript('test "t"\nstep "s"\n press Enter\n sleep 1s', { file: "scripts/shared/login.aite" }); expect(withFile.ok && withFile.script.steps[0]!.commands.map((c) => c.file)).toEqual(["scripts/shared/login.aite", "scripts/shared/login.aite"]); const without = parseScript('test "t"\nstep "s"\n press Enter'); expect(without.ok && "file" in without.script.steps[0]!.commands[0]!).toBe(false); });});
describe("explicit loose text for verify: text-i: and text~i:", () => { it.each<[string, object]>([ ['verify text-i:"1 mallbucks"', { verb: "verify", kind: "text", value: "1 mallbucks", match: "ignoreCase" }], ['verify text~i:"mallbucks"', { verb: "verify", kind: "text", value: "mallbucks", match: "containsIgnoreCase" }], ['verify css:".b" text-i:"5 mallbucks"', { verb: "verify", kind: "targetText", target: { css: ".b" }, value: "5 mallbucks", match: "ignoreCase" }], ['verify css:".b" text~i:"mall"', { verb: "verify", kind: "targetText", target: { css: ".b" }, value: "mall", match: "containsIgnoreCase" }], ['verifyNot text-i:"error"', { verb: "verifyNot", kind: "text", value: "error", match: "ignoreCase" }], ['verify text-i:"x" within:"Box" timeout:2s', { kind: "text", match: "ignoreCase", within: "Box", timeoutMs: 2000 }], ])("%s", (text, expected) => { expect(one(text)).toMatchObject(expected); }); it("verify text: stays exact: it has no match field", () => { expect(one('verify text:"x"')).not.toHaveProperty("match"); expect(one('verify css:".b" text:"x"')).not.toHaveProperty("match"); }); it.each<[string, string]>([ ['verify text-i:"a" text~i:"b"', "this combination of verify is not in the script format"], ['verify text:"a" text-i:"b"', "this combination of verify is not in the script format"], ['verify css:".b" text:"a" text-i:"b"', "this combination of verify is not in the script format"], ['verify text-i:"a" title:"b"', "this combination of verify is not in the script format"], ['verify css:".b" text-i:"a" enabled', "this combination of verify is not in the script format"], ['verify css:".b" text-i:"a" value:"b"', "this combination of verify is not in the script format"], ])("%s is refused", (command, message) => { expect(errors(wrap(command))[0]).toMatchObject({ code: "E003", message }); }); it("the loose spellings are only for verify: elsewhere they are unknown target keys", () => { expect(errors(wrap('click text-i:"x"'))[0]).toMatchObject({ code: "E005" }); expect(errors(wrap('wait text~i:"x"'))[0]).toMatchObject({ code: "E005" }); });});
describe("press: the key must be a name the runner knows, or one character", () => { it.each(["Enter", "Tab", "Escape", "ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Backspace", "Delete", "Space", "Home", "End", "PageUp", "PageDown", "a", "7", "é"])("%s is accepted", (key) => { expect(one(`press ${key}`)).toMatchObject({ verb: "press", key }); }); it.each(["Banana", "enter", "Ctrl+A", "F13", "ab"])("%s is refused before anything runs, naming the keys that exist", (key) => { expect(errors(wrap(`press ${key}`))[0]).toMatchObject({ code: "E003", line: 3, message: expect.stringContaining(`unknown key "${key}"`) }); expect(errors(wrap(`press ${key}`))[0]!.message).toContain("Enter, Tab, Escape"); }); it("a quoted key goes through the same check", () => { expect(one('press "Enter"')).toMatchObject({ key: "Enter" }); expect(errors(wrap('press "Banana"'))[0]).toMatchObject({ code: "E003" }); });});