fix(setup-wizard): address review — icon, CSS scope, E2E coverage

- Use a plain emoji glyph for the password visibility toggle instead
  of embedded SVG paths, matching the same pragmatic approach already
  used for the browser build's menu icons in this codebase.
- Scope the wider label width to the E2EE dialogue instead of
  changing it for every InputRow in every Svelte dialogue.
- Extend the existing CouchDB manual setup E2E workflow to assert the
  passphrase and Obfuscate Properties fields are hidden until
  encryption is enabled, then appear, and that the passphrase value
  survives toggling its visibility.
This commit is contained in:
Zeedif
2026-08-26 20:45:44 -06:00
parent 6b37ea8778
commit 2c35f45765
4 changed files with 126 additions and 107 deletions
@@ -48,80 +48,94 @@
} }
</script> </script>
<DialogHeader title={translateMessage("End-to-End Encryption")} /> <div class="sls-e2ee-dialog">
<Guidance>{translateMessage("Please configure your end-to-end encryption settings.")}</Guidance> <DialogHeader title={translateMessage("End-to-End Encryption")} />
<InputRow label={translateMessage("End-to-End Encryption")}> <Guidance>{translateMessage("Please configure your end-to-end encryption settings.")}</Guidance>
<input type="checkbox" bind:checked={encryptionSettings.encrypt} /> <InputRow label={translateMessage("End-to-End Encryption")}>
</InputRow> <input type="checkbox" bind:checked={encryptionSettings.encrypt} />
<InfoNote title={translateMessage("Strongly Recommended")}>
{translateMessage(
"Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote server. This means that even if someone gains access to the server, they won't be able to read your data without the passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices."
)}
<br />
{translateMessage(
"Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future."
)}
</InfoNote>
{#if encryptionSettings.encrypt}
<InputRow label={translateMessage("Passphrase")}>
<Password
name="e2ee-passphrase"
placeholder={translateMessage("Enter your passphrase")}
bind:value={encryptionSettings.passphrase}
required
/>
</InputRow> </InputRow>
<InfoNote title={translateMessage("Strongly Recommended")}>
{translateMessage(
"Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote server. This means that even if someone gains access to the server, they won't be able to read your data without the passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices."
)}
<br />
{translateMessage(
"Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future."
)}
</InfoNote>
{#if encryptionSettings.encrypt}
<InputRow label={translateMessage("Passphrase")}>
<Password
name="e2ee-passphrase"
placeholder={translateMessage("Enter your passphrase")}
bind:value={encryptionSettings.passphrase}
required
/>
</InputRow>
<InfoNote warning>
{translateMessage(
"This setting must be the same even when connecting to multiple synchronisation destinations."
)}
</InfoNote>
<InputRow label={translateMessage("Obfuscate Properties")}>
<input type="checkbox" bind:checked={encryptionSettings.usePathObfuscation} />
</InputRow>
<InfoNote>
{translateMessage(
"Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data."
)}
</InfoNote>
{/if}
<ExtraItems title={translateMessage("Advanced")}>
<InputRow label={translateMessage("Encryption Algorithm")}>
<select bind:value={encryptionSettings.E2EEAlgorithm} disabled={!encryptionSettings.encrypt}>
{#each Object.values(E2EEAlgorithms) as alg}
<option value={alg}>{E2EEAlgorithmNames[alg] ?? alg}</option>
{/each}
</select>
</InputRow>
<InfoNote>
{translateMessage(
"In most cases, you should stick with the default algorithm (${algorithm}), This setting is only required if you have an existing Vault encrypted in a different format.",
{ algorithm: E2EEAlgorithmNames[DEFAULT_SETTINGS.E2EEAlgorithm] }
)}
</InfoNote>
<InfoNote warning>
{translateMessage(
"Changing the encryption algorithm will prevent access to any data previously encrypted with a different algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your data."
)}
</InfoNote>
</ExtraItems>
<InfoNote warning> <InfoNote warning>
{translateMessage( <p>
"This setting must be the same even when connecting to multiple synchronisation destinations." {translateMessage(
)} "Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process actually commences. This is a security measure designed to protect your data."
)}
</p>
<p>
{translateMessage(
"Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted."
)} <br /><br />
{translateMessage("Please understand that this is intended behaviour.")}
</p>
</InfoNote> </InfoNote>
<InputRow label={translateMessage("Obfuscate Properties")}>
<input type="checkbox" bind:checked={encryptionSettings.usePathObfuscation} />
</InputRow>
<InfoNote>
{translateMessage(
"Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data."
)}
</InfoNote>
{/if}
<ExtraItems title={translateMessage("Advanced")}> <UserDecisions>
<InputRow label={translateMessage("Encryption Algorithm")}> <Decision title={translateMessage("Proceed")} important disabled={!e2eeValid} commit={() => commit()} />
<select bind:value={encryptionSettings.E2EEAlgorithm} disabled={!encryptionSettings.encrypt}> <Decision title={translateMessage("Cancel")} commit={() => setResult(TYPE_CANCELLED)} />
{#each Object.values(E2EEAlgorithms) as alg} </UserDecisions>
<option value={alg}>{E2EEAlgorithmNames[alg] ?? alg}</option> </div>
{/each}
</select>
</InputRow>
<InfoNote>
{translateMessage(
"In most cases, you should stick with the default algorithm (${algorithm}), This setting is only required if you have an existing Vault encrypted in a different format.",
{ algorithm: E2EEAlgorithmNames[DEFAULT_SETTINGS.E2EEAlgorithm] }
)}
</InfoNote>
<InfoNote warning>
{translateMessage(
"Changing the encryption algorithm will prevent access to any data previously encrypted with a different algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your data."
)}
</InfoNote>
</ExtraItems>
<InfoNote warning> <style>
<p> .sls-e2ee-dialog {
{translateMessage( display: flex;
"Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process actually commences. This is a security measure designed to protect your data." flex-direction: column;
)} gap: 0.5em;
</p> }
<p> :global(.dialog-host .sls-e2ee-dialog label > span) {
{translateMessage( width: auto;
"Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted." min-width: 8em;
)} <br /><br /> }
{translateMessage("Please understand that this is intended behaviour.")} </style>
</p>
</InfoNote>
<UserDecisions>
<Decision title={translateMessage("Proceed")} important disabled={!e2eeValid} commit={() => commit()} />
<Decision title={translateMessage("Cancel")} commit={() => setResult(TYPE_CANCELLED)} />
</UserDecisions>
@@ -110,7 +110,7 @@
.dialog-host :global(label > span) { .dialog-host :global(label > span) {
display: block; display: block;
min-width: 8em; width: 8em;
} }
.dialog-host :global(.note), .dialog-host :global(.note),
@@ -41,37 +41,7 @@
{disabled} {disabled}
onclick={() => (showPassword = !showPassword)} onclick={() => (showPassword = !showPassword)}
> >
{#if showPassword} 👁️
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" />
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" />
<path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" />
<line x1="2" x2="22" y1="2" y2="22" />
</svg>
{:else}
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
{/if}
</button> </button>
<style> <style>
@@ -85,11 +55,10 @@
background: transparent; background: transparent;
border: none; border: none;
box-shadow: none; box-shadow: none;
color: var(--text-muted); line-height: 1;
cursor: pointer; cursor: pointer;
} }
.sls-password-toggle:hover { .sls-password-toggle:hover {
color: var(--text-normal);
background: var(--background-modifier-hover); background: var(--background-modifier-hover);
} }
</style> </style>
@@ -117,19 +117,55 @@ async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig,
const encryption = modalByTitle(page, "End-to-End Encryption"); const encryption = modalByTitle(page, "End-to-End Encryption");
await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs }); await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs });
assertEqual(
await encryption.locator('input[name="e2ee-passphrase"]').count(),
0,
"The passphrase field was present before end-to-end encryption was enabled."
);
assertEqual(
await encryption.locator("label.row").filter({ hasText: "Obfuscate Properties" }).count(),
0,
"The Obfuscate Properties row was present before end-to-end encryption was enabled."
);
await encryption await encryption
.locator("label.row") .locator("label.row")
.filter({ hasText: "End-to-End Encryption" }) .filter({ hasText: "End-to-End Encryption" })
.locator('input[type="checkbox"]') .locator('input[type="checkbox"]')
.first() .first()
.check({ timeout: uiTimeoutMs }); .check({ timeout: uiTimeoutMs });
const passphraseInput = encryption.locator('input[name="e2ee-passphrase"]');
await passphraseInput.waitFor({ state: "visible", timeout: uiTimeoutMs });
await encryption await encryption
.locator("label.row") .locator("label.row")
.filter({ hasText: "Obfuscate Properties" }) .filter({ hasText: "Obfuscate Properties" })
.locator('input[type="checkbox"]') .locator('input[type="checkbox"]')
.first() .first()
.check({ timeout: uiTimeoutMs }); .check({ timeout: uiTimeoutMs });
await encryption.locator('input[name="e2ee-passphrase"]').fill(randomBytes(24).toString("base64url")); const passphraseValue = randomBytes(24).toString("base64url");
await passphraseInput.fill(passphraseValue);
const passwordToggle = encryption.locator("button.sls-password-toggle");
await passwordToggle.click({ timeout: uiTimeoutMs });
assertEqual(
await passphraseInput.getAttribute("type"),
"text",
"Toggling visibility did not reveal the passphrase."
);
assertEqual(
await passphraseInput.inputValue(),
passphraseValue,
"Toggling visibility changed the passphrase value."
);
await passwordToggle.click({ timeout: uiTimeoutMs });
assertEqual(
await passphraseInput.getAttribute("type"),
"password",
"Toggling visibility again did not re-mask the passphrase."
);
assertEqual(
await passphraseInput.inputValue(),
passphraseValue,
"Re-masking the passphrase changed its value."
);
}); });
screenshots.push(await captureGuideDialogue(port, "guide-couchdb-manual-encryption.png", "End-to-End Encryption")); screenshots.push(await captureGuideDialogue(port, "guide-couchdb-manual-encryption.png", "End-to-End Encryption"));
await withObsidianPage(port, async (page) => { await withObsidianPage(port, async (page) => {