mirror of
https://github.com/TriliumNext/Trilium.git
synced 2026-10-12 02:09:59 +00:00
refactor(web-clipper): rewrite the options page in Preact, drop cash-dom
The options page was the last user of cash-dom. It is now an `Options` component holding the shared message, a `DesktopPortForm`, and a `ServerSetup` that shows either the configured server or the `ServerLoginForm`. The login request is `requestToken()`, which returns how the login ended (token, rejected code, rejected credentials, unexpected status, network error) and leaves the wording to the form. The inline styles moved to options.css. Behavior kept as it was, except: - The success message after logging in stays visible. The old page called restoreOptions() right after showing it, which hid it again. - "remove the current setup" is a link with a click handler instead of `href="javascript:"`. - The server section renders once the stored settings are read, instead of showing the login form until they are. Without cash-dom the options bundle goes from 19.5 kB to 4.9 kB. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8b1679ff74
commit
417d571fac
@@ -2,64 +2,13 @@
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||
<meta content="utf-8" http-equiv="encoding">
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<div id="error-message" style="font-weight: bold; color: red; display: none;"></div>
|
||||
<div id="success-message" style="font-weight: bold; color: green; display: none;"></div>
|
||||
|
||||
<h2>Trilium desktop instance</h2>
|
||||
|
||||
<p>Web clipper by default tries to find a running desktop instance on port 37840. If you configured your Trilium desktop app to run on a different port (for example with the <code>TRILIUM_PORT</code> environment variable), you can specify it here (otherwise keep it empty).</p>
|
||||
|
||||
<form id="trilium-desktop-setup-form">
|
||||
<p>
|
||||
<label for="trilium-desktop-port" style="font-weight: bold;">Trilium desktop port: </label>
|
||||
|
||||
<input type="text" id="trilium-desktop-port" size="6" style="text-align: right;"/> (normally keep this empty)
|
||||
</p>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
<h2>Trilium server instance</h2>
|
||||
|
||||
<p>If you have a server instance set up, you can optionally configure it as a fail over target for the clipped notes. Desktop instance will still be given priority, but in cases that the desktop instance is not available (e.g. it's not running), web clipper will send the notes to the server instance instead.</p>
|
||||
|
||||
<div id="trilium-server-configured" style="display: none;">
|
||||
<strong>Trilium server instance has been already configured to <a id="trilium-server-link" href=""></a>.</strong>
|
||||
|
||||
<p>You can also <a id="reset-trilium-server-setup" href="javascript:">remove the current setup</a> and configure it again.</p>
|
||||
</div>
|
||||
|
||||
<form id="trilium-server-setup-form" style="display: none;">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Trilium server URL:</th>
|
||||
<td><input type="text" id="trilium-server-url"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Password:</th>
|
||||
<td><input type="password" id="trilium-server-password"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Authentication code:</th>
|
||||
<td><input type="text" id="trilium-server-totp" autocomplete="one-time-code"/> (only if two-factor authentication is enabled)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td><input type="submit" value="Login to the server instance"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>Note that the entered password is not stored anywhere, it will be only used to retrieve an authorization token from the server instance which will be then used to send the clipped notes.</p>
|
||||
</form>
|
||||
|
||||
<script type="module" src="index.ts"></script>
|
||||
|
||||
<script type="module" src="main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import $ from "cash-dom";
|
||||
|
||||
const $triliumServerUrl = $("#trilium-server-url");
|
||||
const $triliumServerPassword = $("#trilium-server-password");
|
||||
const $triliumServerTotp = $("#trilium-server-totp");
|
||||
|
||||
const $errorMessage = $("#error-message");
|
||||
const $successMessage = $("#success-message");
|
||||
|
||||
function showError(message) {
|
||||
$errorMessage.html(message).show();
|
||||
$successMessage.hide();
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
$successMessage.html(message).show();
|
||||
$errorMessage.hide();
|
||||
}
|
||||
|
||||
async function saveTriliumServerSetup(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (($triliumServerUrl.val() as string | undefined)?.trim().length === 0
|
||||
|| ($triliumServerPassword.val() as string | undefined)?.trim().length === 0) {
|
||||
showError("One or more mandatory inputs are missing. Please fill in server URL and password.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const triliumServerUrl = ($triliumServerUrl.val() as string).trim().replace(/\/+$/, '');
|
||||
|
||||
let resp;
|
||||
|
||||
try {
|
||||
resp = await fetch(`${triliumServerUrl}/api/login/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: $triliumServerPassword.val(),
|
||||
totpToken: ($triliumServerTotp.val() as string | undefined)?.trim()
|
||||
})
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
showError(`Unknown error: ${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resp.status === 401) {
|
||||
const { factor } = await resp.json().catch(() => ({}));
|
||||
|
||||
if (factor === "totp") {
|
||||
const totpWasEntered = ($triliumServerTotp.val() as string | undefined)?.trim();
|
||||
$triliumServerTotp.val("").trigger("focus");
|
||||
showError(totpWasEntered
|
||||
? "Incorrect authentication code."
|
||||
: "Two-factor authentication is enabled. Enter the code from your authenticator app or a recovery code.");
|
||||
} else {
|
||||
showError("Incorrect credentials.");
|
||||
}
|
||||
}
|
||||
else if (resp.status !== 200) {
|
||||
showError(`Unrecognised response with status code ${ resp.status}`);
|
||||
}
|
||||
else {
|
||||
const json = await resp.json();
|
||||
|
||||
showSuccess("Authentication against Trilium server has been successful.");
|
||||
|
||||
$triliumServerPassword.val('');
|
||||
|
||||
browser.storage.sync.set({
|
||||
triliumServerUrl: triliumServerUrl,
|
||||
authToken: json.token
|
||||
});
|
||||
|
||||
await restoreOptions();
|
||||
}
|
||||
}
|
||||
|
||||
const $triliumServerSetupForm = $("#trilium-server-setup-form");
|
||||
const $triliumServerConfiguredDiv = $("#trilium-server-configured");
|
||||
const $triliumServerLink = $("#trilium-server-link");
|
||||
const $resetTriliumServerSetupLink = $("#reset-trilium-server-setup");
|
||||
|
||||
$resetTriliumServerSetupLink.on("click", e => {
|
||||
e.preventDefault();
|
||||
|
||||
browser.storage.sync.set({
|
||||
triliumServerUrl: '',
|
||||
authToken: ''
|
||||
});
|
||||
|
||||
restoreOptions();
|
||||
});
|
||||
|
||||
$triliumServerSetupForm.on("submit", saveTriliumServerSetup);
|
||||
|
||||
const $triliumDesktopPort = $("#trilium-desktop-port");
|
||||
const $triilumDesktopSetupForm = $("#trilium-desktop-setup-form");
|
||||
|
||||
$triilumDesktopSetupForm.on("submit", e => {
|
||||
e.preventDefault();
|
||||
|
||||
const port = ($triliumDesktopPort.val() as string | undefined ?? "").trim();
|
||||
const portNum = parseInt(port, 10);
|
||||
|
||||
if (port && (isNaN(portNum) || portNum <= 0 || portNum >= 65536)) {
|
||||
showError(`Please enter valid port number.`);
|
||||
return;
|
||||
}
|
||||
|
||||
browser.storage.sync.set({
|
||||
triliumDesktopPort: port
|
||||
});
|
||||
|
||||
showSuccess(`Port number has been saved.`);
|
||||
});
|
||||
|
||||
async function restoreOptions() {
|
||||
const {triliumServerUrl} = await browser.storage.sync.get<{ triliumServerUrl: string }>("triliumServerUrl");
|
||||
const {authToken} = await browser.storage.sync.get<{ authToken: string }>("authToken");
|
||||
|
||||
$errorMessage.hide();
|
||||
$successMessage.hide();
|
||||
|
||||
$triliumServerUrl.val('');
|
||||
$triliumServerPassword.val('');
|
||||
$triliumServerTotp.val('');
|
||||
|
||||
if (triliumServerUrl && authToken) {
|
||||
$triliumServerSetupForm.hide();
|
||||
$triliumServerConfiguredDiv.show();
|
||||
|
||||
$triliumServerLink
|
||||
.attr("href", triliumServerUrl)
|
||||
.text(triliumServerUrl);
|
||||
}
|
||||
else {
|
||||
$triliumServerSetupForm.show();
|
||||
$triliumServerConfiguredDiv.hide();
|
||||
}
|
||||
|
||||
const {triliumDesktopPort} = await browser.storage.sync.get<{ triliumDesktopPort: string }>("triliumDesktopPort");
|
||||
$triliumDesktopPort.val(triliumDesktopPort);
|
||||
}
|
||||
|
||||
$(restoreOptions);
|
||||
@@ -0,0 +1,245 @@
|
||||
import "./options.css";
|
||||
|
||||
import { render } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
type Message = { kind: "error" | "success", text: string } | null;
|
||||
type ShowMessage = (message: Message) => void;
|
||||
|
||||
const root = document.getElementById("root");
|
||||
if (root) {
|
||||
render(<Options />, root);
|
||||
}
|
||||
|
||||
export function Options() {
|
||||
const [ message, setMessage ] = useState<Message>(null);
|
||||
|
||||
return (
|
||||
<div className="options">
|
||||
{message && <div className={`message message-${message.kind}`}>{message.text}</div>}
|
||||
|
||||
<h2>Trilium desktop instance</h2>
|
||||
|
||||
<p>
|
||||
Web clipper by default tries to find a running desktop instance on port 37840. If you
|
||||
configured your Trilium desktop app to run on a different port (for example with
|
||||
the <code>TRILIUM_PORT</code> environment variable), you can specify it here
|
||||
(otherwise keep it empty).
|
||||
</p>
|
||||
|
||||
<DesktopPortForm showMessage={setMessage} />
|
||||
|
||||
<h2>Trilium server instance</h2>
|
||||
|
||||
<p>
|
||||
If you have a server instance set up, you can optionally configure it as a fail over
|
||||
target for the clipped notes. Desktop instance will still be given priority, but in
|
||||
cases that the desktop instance is not available (e.g. it's not running), web clipper
|
||||
will send the notes to the server instance instead.
|
||||
</p>
|
||||
|
||||
<ServerSetup showMessage={setMessage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DesktopPortForm({ showMessage }: { showMessage: ShowMessage }) {
|
||||
const [ port, setPort ] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
void browser.storage.sync.get<{ triliumDesktopPort?: string }>("triliumDesktopPort")
|
||||
.then(({ triliumDesktopPort }) => setPort(triliumDesktopPort ?? ""));
|
||||
}, []);
|
||||
|
||||
async function save(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const trimmedPort = port.trim();
|
||||
const portNum = parseInt(trimmedPort, 10);
|
||||
if (trimmedPort && (isNaN(portNum) || portNum <= 0 || portNum >= 65536)) {
|
||||
showMessage({ kind: "error", text: "Please enter valid port number." });
|
||||
return;
|
||||
}
|
||||
|
||||
await browser.storage.sync.set({ triliumDesktopPort: trimmedPort });
|
||||
showMessage({ kind: "success", text: "Port number has been saved." });
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={save}>
|
||||
<p>
|
||||
<label for="trilium-desktop-port">Trilium desktop port: </label>
|
||||
<input
|
||||
type="text"
|
||||
id="trilium-desktop-port"
|
||||
className="desktop-port"
|
||||
size={6}
|
||||
value={port}
|
||||
onInput={(e) => setPort(e.currentTarget.value)}
|
||||
/>
|
||||
{" "}(normally keep this empty)
|
||||
</p>
|
||||
|
||||
<input type="submit" value="Save" />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function ServerSetup({ showMessage }: { showMessage: ShowMessage }) {
|
||||
/** The configured server's URL, `null` when none is, and `undefined` until storage is read. */
|
||||
const [ configuredUrl, setConfiguredUrl ] = useState<string | null>();
|
||||
|
||||
useEffect(() => {
|
||||
void browser.storage.sync.get<{ triliumServerUrl?: string, authToken?: string }>([ "triliumServerUrl", "authToken" ])
|
||||
.then(({ triliumServerUrl, authToken }) => {
|
||||
setConfiguredUrl(triliumServerUrl && authToken ? triliumServerUrl : null);
|
||||
});
|
||||
}, []);
|
||||
|
||||
async function reset(e: Event) {
|
||||
e.preventDefault();
|
||||
await browser.storage.sync.set({ triliumServerUrl: "", authToken: "" });
|
||||
setConfiguredUrl(null);
|
||||
showMessage(null);
|
||||
}
|
||||
|
||||
if (configuredUrl === undefined) return null;
|
||||
|
||||
if (configuredUrl) {
|
||||
return (
|
||||
<div>
|
||||
<strong>
|
||||
Trilium server instance has been already configured to <a href={configuredUrl}>{configuredUrl}</a>.
|
||||
</strong>
|
||||
|
||||
<p>You can also <a href="#" onClick={reset}>remove the current setup</a> and configure it again.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <ServerLoginForm showMessage={showMessage} onLoggedIn={setConfiguredUrl} />;
|
||||
}
|
||||
|
||||
function ServerLoginForm({ showMessage, onLoggedIn }: {
|
||||
showMessage: ShowMessage;
|
||||
onLoggedIn: (serverUrl: string) => void;
|
||||
}) {
|
||||
const [ url, setUrl ] = useState("");
|
||||
const [ password, setPassword ] = useState("");
|
||||
const [ totp, setTotp ] = useState("");
|
||||
const totpRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
async function login(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!url.trim() || !password.trim()) {
|
||||
showMessage({ kind: "error", text: "One or more mandatory inputs are missing. Please fill in server URL and password." });
|
||||
return;
|
||||
}
|
||||
|
||||
const serverUrl = url.trim().replace(/\/+$/, "");
|
||||
const result = await requestToken(serverUrl, password, totp.trim());
|
||||
|
||||
switch (result.kind) {
|
||||
case "token":
|
||||
await browser.storage.sync.set({ triliumServerUrl: serverUrl, authToken: result.token });
|
||||
showMessage({ kind: "success", text: "Authentication against Trilium server has been successful." });
|
||||
onLoggedIn(serverUrl);
|
||||
break;
|
||||
case "totp-rejected":
|
||||
setTotp("");
|
||||
totpRef.current?.focus();
|
||||
showMessage({
|
||||
kind: "error",
|
||||
text: totp.trim()
|
||||
? "Incorrect authentication code."
|
||||
: "Two-factor authentication is enabled. Enter the code from your authenticator app or a recovery code."
|
||||
});
|
||||
break;
|
||||
case "rejected":
|
||||
showMessage({ kind: "error", text: "Incorrect credentials." });
|
||||
break;
|
||||
case "unexpected-status":
|
||||
showMessage({ kind: "error", text: `Unrecognised response with status code ${result.status}` });
|
||||
break;
|
||||
case "network-error":
|
||||
showMessage({ kind: "error", text: `Unknown error: ${result.message}` });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={login}>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Trilium server URL:</th>
|
||||
<td><input type="text" value={url} onInput={(e) => setUrl(e.currentTarget.value)} /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Password:</th>
|
||||
<td><input type="password" value={password} onInput={(e) => setPassword(e.currentTarget.value)} /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Authentication code:</th>
|
||||
<td>
|
||||
<input
|
||||
ref={totpRef}
|
||||
type="text"
|
||||
autocomplete="one-time-code"
|
||||
value={totp}
|
||||
onInput={(e) => setTotp(e.currentTarget.value)}
|
||||
/>
|
||||
{" "}(only if two-factor authentication is enabled)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td><input type="submit" value="Login to the server instance" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
Note that the entered password is not stored anywhere, it will be only used to retrieve
|
||||
an authorization token from the server instance which will be then used to send the
|
||||
clipped notes.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
type TokenResult =
|
||||
| { kind: "token", token: string }
|
||||
| { kind: "totp-rejected" }
|
||||
| { kind: "rejected" }
|
||||
| { kind: "unexpected-status", status: number }
|
||||
| { kind: "network-error", message: string };
|
||||
|
||||
/** Logs in to a Trilium server to obtain an ETAPI token for the clipper. */
|
||||
export async function requestToken(serverUrl: string, password: string, totpToken: string): Promise<TokenResult> {
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(`${serverUrl}/api/login/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ password, totpToken })
|
||||
});
|
||||
} catch (e) {
|
||||
return { kind: "network-error", message: e instanceof Error ? e.message : String(e) };
|
||||
}
|
||||
|
||||
if (resp.status === 401) {
|
||||
// Servers before the factor was reported answer with a plain string.
|
||||
const { factor } = await resp.json().catch(() => ({}));
|
||||
return factor === "totp" ? { kind: "totp-rejected" } : { kind: "rejected" };
|
||||
}
|
||||
|
||||
if (resp.status !== 200) {
|
||||
return { kind: "unexpected-status", status: resp.status };
|
||||
}
|
||||
|
||||
const { token } = await resp.json();
|
||||
return { kind: "token", token };
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.options {
|
||||
.message {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message-error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.message-success {
|
||||
color: green;
|
||||
}
|
||||
|
||||
label[for="trilium-desktop-port"] {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.desktop-port {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { render } from "preact";
|
||||
import { act } from "preact/test-utils";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fakeBrowser } from "wxt/testing/fake-browser";
|
||||
|
||||
import { Options, requestToken } from "./main";
|
||||
|
||||
const fetchMock = vi.fn<(url: string, init?: RequestInit) => Promise<Response>>();
|
||||
let container: HTMLElement;
|
||||
|
||||
describe("options page", () => {
|
||||
beforeEach(() => {
|
||||
fakeBrowser.reset();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await act(() => render(null, container));
|
||||
container.remove();
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("saves the desktop port and rejects an invalid one", async () => {
|
||||
await browser.storage.sync.set({ triliumDesktopPort: "12345" });
|
||||
await renderOptions();
|
||||
expect(input("#trilium-desktop-port").value).toBe("12345");
|
||||
|
||||
await type("#trilium-desktop-port", "70000");
|
||||
await submit(0);
|
||||
expect(message()).toEqual({ className: "message message-error", text: "Please enter valid port number." });
|
||||
expect(await stored("triliumDesktopPort")).toBe("12345");
|
||||
|
||||
await type("#trilium-desktop-port", " 37841 ");
|
||||
await submit(0);
|
||||
expect(message()).toEqual({ className: "message message-success", text: "Port number has been saved." });
|
||||
expect(await stored("triliumDesktopPort")).toBe("37841");
|
||||
|
||||
await type("#trilium-desktop-port", "");
|
||||
await submit(0);
|
||||
expect(await stored("triliumDesktopPort")).toBe("");
|
||||
});
|
||||
|
||||
it("logs in to a server, asking for the code when it requires one", async () => {
|
||||
await renderOptions();
|
||||
await submit(1);
|
||||
expect(message()?.text).toContain("Please fill in server URL and password.");
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
|
||||
await type("input[type=text]:not(#trilium-desktop-port)", " https://trilium.example/// ");
|
||||
await type("input[type=password]", "secret");
|
||||
fetchMock.mockResolvedValueOnce(Response.json({ message: "Incorrect credential", factor: "totp" }, { status: 401 }));
|
||||
await submit(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith("https://trilium.example/api/login/token", expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({ password: "secret", totpToken: "" })
|
||||
}));
|
||||
expect(message()?.text).toMatch(/^Two-factor authentication is enabled/);
|
||||
expect(document.activeElement).toBe(input("input[autocomplete=one-time-code]"));
|
||||
|
||||
await type("input[autocomplete=one-time-code]", "000000");
|
||||
fetchMock.mockResolvedValueOnce(Response.json({ message: "Incorrect credential", factor: "totp" }, { status: 401 }));
|
||||
await submit(1);
|
||||
expect(message()?.text).toBe("Incorrect authentication code.");
|
||||
expect(input("input[autocomplete=one-time-code]").value).toBe("");
|
||||
|
||||
await type("input[autocomplete=one-time-code]", "123456");
|
||||
fetchMock.mockResolvedValueOnce(Response.json({ token: "etapi-token" }));
|
||||
await submit(1);
|
||||
expect(message()).toEqual({
|
||||
className: "message message-success",
|
||||
text: "Authentication against Trilium server has been successful."
|
||||
});
|
||||
expect(await stored("triliumServerUrl")).toBe("https://trilium.example");
|
||||
expect(await stored("authToken")).toBe("etapi-token");
|
||||
expect(container.querySelector("strong a")?.getAttribute("href")).toBe("https://trilium.example");
|
||||
|
||||
await act(async () => {
|
||||
const resetLink = [ ...container.querySelectorAll("a") ].find((a) => a.textContent === "remove the current setup");
|
||||
expect(resetLink).toBeDefined();
|
||||
resetLink?.click();
|
||||
});
|
||||
await flush();
|
||||
expect(await stored("authToken")).toBe("");
|
||||
expect(message()).toBeNull();
|
||||
expect(container.querySelector("input[type=password]")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("shows the configured server, which needs both an address and a token", async () => {
|
||||
await browser.storage.sync.set({ triliumServerUrl: "https://trilium.example", authToken: "" });
|
||||
await renderOptions();
|
||||
expect(container.querySelector("strong")).toBeNull();
|
||||
expect(container.querySelector("input[type=password]")).not.toBeNull();
|
||||
await act(() => render(null, container));
|
||||
|
||||
await browser.storage.sync.set({ authToken: "t" });
|
||||
await renderOptions();
|
||||
expect(container.querySelector("strong")?.textContent)
|
||||
.toBe("Trilium server instance has been already configured to https://trilium.example.");
|
||||
expect(container.querySelector("input[type=password]")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestToken", () => {
|
||||
beforeEach(() => vi.stubGlobal("fetch", fetchMock));
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it("tells each way the login can end apart", async () => {
|
||||
fetchMock.mockResolvedValueOnce(Response.json({ token: "t" }));
|
||||
expect(await requestToken("https://s", "p", "")).toEqual({ kind: "token", token: "t" });
|
||||
|
||||
fetchMock.mockResolvedValueOnce(Response.json({ message: "Incorrect credential", factor: "password" }, { status: 401 }));
|
||||
expect(await requestToken("https://s", "p", "")).toEqual({ kind: "rejected" });
|
||||
|
||||
// Servers that do not report the failed factor answer with a plain string.
|
||||
fetchMock.mockResolvedValueOnce(new Response("Incorrect credential", { status: 401 }));
|
||||
expect(await requestToken("https://s", "p", "")).toEqual({ kind: "rejected" });
|
||||
|
||||
fetchMock.mockResolvedValueOnce(new Response("", { status: 502 }));
|
||||
expect(await requestToken("https://s", "p", "")).toEqual({ kind: "unexpected-status", status: 502 });
|
||||
|
||||
fetchMock.mockRejectedValueOnce(new TypeError("Failed to fetch"));
|
||||
expect(await requestToken("https://s", "p", "")).toEqual({ kind: "network-error", message: "Failed to fetch" });
|
||||
});
|
||||
});
|
||||
|
||||
async function renderOptions() {
|
||||
await act(() => render(<Options />, container));
|
||||
await flush();
|
||||
}
|
||||
|
||||
/** Lets the storage reads and writes the effects and handlers start settle. */
|
||||
async function flush() {
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
});
|
||||
}
|
||||
|
||||
function input(selector: string) {
|
||||
const element = container.querySelector<HTMLInputElement>(selector);
|
||||
expect(element, selector).not.toBeNull();
|
||||
return element as HTMLInputElement;
|
||||
}
|
||||
|
||||
async function type(selector: string, value: string) {
|
||||
const element = input(selector);
|
||||
await act(() => {
|
||||
element.value = value;
|
||||
element.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
async function submit(formIndex: number) {
|
||||
const form = container.querySelectorAll("form")[formIndex];
|
||||
expect(form).toBeDefined();
|
||||
await act(() => {
|
||||
form?.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
|
||||
});
|
||||
await flush();
|
||||
}
|
||||
|
||||
function message() {
|
||||
const element = container.querySelector(".message");
|
||||
return element ? { className: element.className, text: element.textContent } : null;
|
||||
}
|
||||
|
||||
async function stored(key: string) {
|
||||
return (await browser.storage.sync.get<Record<string, unknown>>(key))[key];
|
||||
}
|
||||
@@ -23,7 +23,6 @@
|
||||
"wxt": "0.21.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"cash-dom": "8.1.5",
|
||||
"preact": "11.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
-8
@@ -1024,9 +1024,6 @@ importers:
|
||||
|
||||
apps/web-clipper:
|
||||
dependencies:
|
||||
cash-dom:
|
||||
specifier: 8.1.5
|
||||
version: 8.1.5
|
||||
preact:
|
||||
specifier: 11.0.0
|
||||
version: 11.0.0([email protected])
|
||||
@@ -6131,9 +6128,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-/BS05CfzyHR5xT2ksKj1sDLPaOv5rSmIwoGxNgdKwUtnIuiJ5neMxVEmZxvfyJiSjGbOMD0Lwe+9v+fszDqHew==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
@@ -17843,8 +17837,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
Reference in New Issue
Block a user