Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.claude/
node_modules/
test-results/
test-results/
coverage/
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ test-coverage: # Run unit tests with coverage
@echo "Running unit tests with coverage.."
bun run test:coverage

test-coverage-html: # Generate HTML coverage report
@echo "Generating HTML coverage report.."
bun test tests/unit --coverage --coverage-reporter=lcov
genhtml coverage/lcov.info --output-directory coverage/html
@echo "Coverage report generated in coverage/html/index.html"

test-integration: # Run integration tests with Playwright
@echo "Running integration tests.."
bun run test:integration
Expand All @@ -45,7 +51,7 @@ test-all: # Run both unit and integration tests
# command, you need to add it to .PHONY below, otherwise it
# won't work. E.g. `make run` wouldn't work if you have
# `run` file in pwd.
.PHONY: help
.PHONY: help dev format lint check test-unit test-coverage test-coverage-html test-integration test-all

# -----------------------------------------------------------
# ----- (Makefile helpers and decoration) --------
Expand Down
15 changes: 14 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[test]
preload = ["./happydom.js"]
3 changes: 3 additions & 0 deletions happydom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { GlobalRegistrator } from "@happy-dom/global-registrator";

GlobalRegistrator.register();
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@happy-dom/global-registrator": "^18.0.1",
"@playwright/test": "^1.54.2"
}
}
19 changes: 4 additions & 15 deletions public_html/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ function setupUI(windowObj = window) {
}
}

function toggleDarkMode() {
const html = document.documentElement;
const storage = typeof window !== "undefined" ? window.localStorage : null;
function toggleDarkMode(windowObj = window) {
const html = windowObj.document.documentElement;
const storage = windowObj?.localStorage ?? null;

if (html.classList.contains("dark-mode")) {
html.classList.remove("dark-mode");
Expand Down Expand Up @@ -241,7 +241,7 @@ function initializeDarkModeToggle(windowObj = window) {
const toggleButton = windowObj.document.querySelector(".dark-mode-toggle");
if (!toggleButton) return;

toggleButton.addEventListener("click", toggleDarkMode);
toggleButton.addEventListener("click", () => toggleDarkMode(windowObj));
}

function isServiceWorkerSupported(windowObj = window) {
Expand Down Expand Up @@ -299,16 +299,6 @@ function toggleSettingsPanel(windowObj = window) {
}
}

function showSaveMessage(windowObj = window) {
const saveMessage = windowObj.document.getElementById("save-message");
if (saveMessage) {
saveMessage.classList.add("visible");
windowObj.setTimeout(() => {
saveMessage.classList.remove("visible");
}, 2000);
}
}

function initializeSettings(windowObj = window) {
registerSettingsComponents(windowObj);

Expand Down Expand Up @@ -532,7 +522,6 @@ if (typeof module !== "undefined" && module.exports) {
registerServiceWorker,
initializePWA,
toggleSettingsPanel,
showSaveMessage,
initializeSettings,
shouldEnableCaching,
SettingsDialog,
Expand Down
140 changes: 140 additions & 0 deletions tests/unit/search-components.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { beforeAll, describe, expect, mock, test } from "bun:test";

const {
SaveMessage,
SettingsDialog,
SettingOption,
} = require("../../public_html/search.js");

describe("Web Components", () => {
beforeAll(() => {
// Register all components once for all tests
customElements.define("save-message", SaveMessage);
customElements.define("settings-dialog", SettingsDialog);
customElements.define("setting-option", SettingOption);
});
describe("SaveMessage", () => {
test("renders with correct class, id, and text content", () => {
document.body.innerHTML = "<save-message></save-message>";

const saveMessage = document.querySelector("save-message");
expect(saveMessage?.classList.contains("save-message")).toBe(true);
expect(saveMessage?.id).toBe("save-message");
expect(saveMessage?.textContent).toBe("✓ Changes saved automatically");
});
});

describe("SettingsDialog", () => {
test("renders with correct structure and content", () => {
document.body.innerHTML = "<settings-dialog></settings-dialog>";

const settingsDialog = document.querySelector("settings-dialog");

const header = settingsDialog?.querySelector(".settings-header");
expect(header).toBeTruthy();

const closeButton = settingsDialog?.querySelector(".close-button");
expect(closeButton).toBeTruthy();
expect(closeButton?.textContent).toBe("×");

const saveMessage = settingsDialog?.querySelector("save-message");
expect(saveMessage).toBeTruthy();

const body = settingsDialog?.querySelector(".settings-body");
expect(body).toBeTruthy();

const bangList = settingsDialog?.querySelector(".bang-list");
expect(bangList).toBeTruthy();
});

test("handles bang selection with localStorage", () => {
const mockStorage = {
getItem: () => "d", // default bang
setItem: mock(),
};
const mockWindow = {
localStorage: mockStorage,
customElements: {
define: () => {},
},
};

document.body.innerHTML = "<settings-dialog></settings-dialog>";

const settingsDialog = document.querySelector("settings-dialog");
settingsDialog.setWindow(mockWindow);
settingsDialog.render();

const googleOption = settingsDialog.querySelector(
'setting-option[bang-key="g"]',
);
expect(googleOption).toBeTruthy();

googleOption.setWindow(mockWindow);
googleOption.connectedCallback(); // Manually trigger render

const googleRadio = googleOption.querySelector('input[value="g"]');
expect(googleRadio).toBeTruthy();

googleRadio.checked = true;
googleRadio.dispatchEvent(new Event("change", { bubbles: true }));

expect(mockStorage.setItem).toHaveBeenCalledWith("default-bang", "g");
});
});

describe("SettingOption", () => {
test("renders with correct structure and attributes", () => {
document.body.innerHTML = `<setting-option bang-key="g" bang-url="https://www.google.com/search?q={{{s}}}" bang-description="Google Search"></setting-option>`;

const settingOption = document.querySelector("setting-option");
settingOption.connectedCallback(); // Manually trigger render

const settingRow = settingOption.querySelector(".setting-row");
expect(settingRow).toBeTruthy();

const bangTrigger = settingOption.querySelector(".bang-trigger");
expect(bangTrigger).toBeTruthy();
expect(bangTrigger.textContent).toBe("g!");

const bangDescription = settingOption.querySelector(".bang-description");
expect(bangDescription).toBeTruthy();
expect(bangDescription.textContent).toBe("Google Search");
expect(bangDescription.title).toBe(
"https://www.google.com/search?q={{{s}}}",
);

const radio = settingOption.querySelector('input[type="radio"]');
expect(radio).toBeTruthy();
expect(radio.name).toBe("default-bang");
expect(radio.value).toBe("g");
expect(radio.checked).toBe(false);
});

test("handles selected attribute and change events", () => {
const mockStorage = {
setItem: mock(),
};
const mockWindow = {
localStorage: mockStorage,
};

document.body.innerHTML = `<setting-option bang-key="w" bang-url="https://en.wikipedia.org/wiki/Special:Search?search={{{s}}}" bang-description="Wikipedia" selected></setting-option>`;

const settingOption = document.querySelector("setting-option");
settingOption.setWindow(mockWindow);
settingOption.connectedCallback(); // Manually trigger render

const radio = settingOption.querySelector('input[type="radio"]');
expect(radio.checked).toBe(true);

radio.checked = false;
radio.dispatchEvent(new Event("change", { bubbles: true }));

radio.checked = true;
radio.dispatchEvent(new Event("change", { bubbles: true }));

expect(mockStorage.setItem).toHaveBeenCalledWith("default-bang", "w");
});
});
});
Loading