diff --git a/test-apps/markdown-only/package.json b/test-apps/markdown-only/package.json
index 8960097a..9082d6d8 100644
--- a/test-apps/markdown-only/package.json
+++ b/test-apps/markdown-only/package.json
@@ -39,7 +39,7 @@
"@embroider/router": "3.0.6",
"@glimmer/component": "^2.1.1",
"decorator-transforms": "2.4.0",
- "ember-primitives": "^0.61.1",
+ "ember-primitives": "^0.62.0",
"ember-source": "7.2.0",
"ember-strict-application-resolver": "0.1.1"
},
diff --git a/test-apps/markdown-only/tests/search-component-test.ts b/test-apps/markdown-only/tests/search-component-test.ts
new file mode 100644
index 00000000..2f0bbfab
--- /dev/null
+++ b/test-apps/markdown-only/tests/search-component-test.ts
@@ -0,0 +1,138 @@
+import {
+ click,
+ currentURL,
+ fillIn,
+ find,
+ triggerKeyEvent,
+ visit,
+ waitFor,
+ waitUntil,
+} from "@ember/test-helpers";
+import { module, test } from "qunit";
+import { setupApplicationTest } from "ember-qunit";
+
+const START = "/Docs/sub-folder/ember-primitives.md";
+
+/**
+ * `mod` is Meta on macOS and Control everywhere else -- the same split
+ * renders in its trigger.
+ */
+const MOD = navigator.userAgent.includes("Mac OS") ? { metaKey: true } : { ctrlKey: true };
+
+module("", function (hooks) {
+ setupApplicationTest(hooks);
+
+ test("the trigger opens a combobox over the docs", async function (assert) {
+ await visit(START);
+
+ assert.dom("dialog.kolay__search").doesNotHaveAttribute("open");
+
+ await click(".kolay__search__trigger");
+
+ assert.dom("dialog.kolay__search").hasAttribute("open");
+ assert.dom(".kolay__search__input").hasAttribute("role", "combobox");
+ assert.dom(".kolay__search__status").hasText(/Search every guide/);
+ });
+
+ test("typing finds a page, and Enter goes to it", async function (assert) {
+ await visit(START);
+ await click(".kolay__search__trigger");
+
+ await fillIn(".kolay__search__input", "resources");
+ // a plain .md page's text is fetched outside the run loop, so settling
+ // isn't enough -- the results render once that fetch resolves
+ await waitFor(".kolay__search__result", { timeout: 5000 });
+
+ assert.dom(".kolay__search__result__title").hasText("ember resources");
+ assert.dom(".kolay__search__result").hasAttribute("role", "option");
+
+ // nothing arrowed: Enter takes the best result
+ await triggerKeyEvent(".kolay__search__input", "keydown", "Enter");
+
+ assert.strictEqual(currentURL(), "/Docs/sub-folder/ember-resources.md");
+ assert.dom("dialog.kolay__search").doesNotHaveAttribute("open");
+ });
+
+ test("a query too short to be useful is not run", async function (assert) {
+ await visit(START);
+ await click(".kolay__search__trigger");
+
+ await fillIn(".kolay__search__input", "re");
+
+ assert.dom(".kolay__search__result").doesNotExist();
+ assert.dom(".kolay__search__status").hasText("Type at least 3 characters.");
+ });
+
+ test("a query that matches nothing says so", async function (assert) {
+ await visit(START);
+ await click(".kolay__search__trigger");
+
+ await fillIn(".kolay__search__input", "zzzznotathinginthedocs");
+ // the status says "Searching…" until every page's text has been fetched
+ await waitUntil(() => !find(".kolay__search__status")?.textContent?.includes("Searching"), {
+ timeout: 5000,
+ });
+
+ assert.dom(".kolay__search__result").doesNotExist();
+ assert.dom(".kolay__search__status").hasText(/No results/);
+ });
+
+ test("the pointer can close it, and the button names the key too", async function (assert) {
+ await visit(START);
+ await click(".kolay__search__trigger");
+
+ assert.dom(".kolay__search__close").hasText("Close Esc");
+ // a click outside is the platform's, off the same attribute
+ assert.dom("dialog.kolay__search").hasAttribute("closedby", "any");
+
+ await click(".kolay__search__close");
+
+ assert.dom("dialog.kolay__search").doesNotHaveAttribute("open");
+ assert.dom(".kolay__search__trigger").isFocused();
+ });
+
+ test("the results collapse when there are none", async function (assert) {
+ await visit(START);
+ await click(".kolay__search__trigger");
+
+ assert.dom(".kolay__search__results").hasAttribute("data-has-results", "false");
+
+ const results = find(".kolay__search__results") as HTMLElement;
+
+ assert.strictEqual(results.getBoundingClientRect().height, 0, "no empty box to scroll");
+
+ await fillIn(".kolay__search__input", "resources");
+ await waitFor(".kolay__search__result", { timeout: 5000 });
+
+ assert.dom(".kolay__search__results").hasAttribute("data-has-results", "true");
+ assert.ok(results.getBoundingClientRect().height > 0, "the results have room");
+ });
+
+ test("the clear button empties the query and gives the caret back", async function (assert) {
+ await visit(START);
+ await click(".kolay__search__trigger");
+
+ assert.dom(".kolay__search__clear").doesNotExist();
+
+ await fillIn(".kolay__search__input", "resources");
+ await waitFor(".kolay__search__result", { timeout: 5000 });
+
+ await click(".kolay__search__clear");
+
+ assert.dom(".kolay__search__input").hasValue("");
+ assert.dom(".kolay__search__input").isFocused();
+ assert.dom(".kolay__search__result").doesNotExist();
+ assert.dom(".kolay__search__clear").doesNotExist();
+ });
+
+ test("the hotkey opens it from anywhere", async function (assert) {
+ await visit(START);
+
+ assert.dom("dialog.kolay__search").doesNotHaveAttribute("open");
+
+ await triggerKeyEvent(document.body, "keydown", "K", MOD);
+
+ assert.dom("dialog.kolay__search").hasAttribute("open");
+ assert.dom(".kolay__search__input").isFocused();
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
index 3fba1ce7..fe692255 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,11 +11,11 @@
"isolatedModules": true,
// @tsconfig/node22
- "module": "node16",
+ "module": "esnext",
"target": "es2022",
"esModuleInterop": true,
"skipLibCheck": true,
- "moduleResolution": "node16",
+ "moduleResolution": "bundler",
// My options
"rootDir": "./src",
@@ -33,6 +33,6 @@
"declaration": true,
// Compat
"allowSyntheticDefaultImports": true,
- "types": ["./docs-app/node_modules/ember-source/types/stable", "unplugin", "@glint/ember-tsc"]
+ "types": ["ember-source/types", "unplugin", "@glint/ember-tsc/types"]
}
}