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
31 changes: 30 additions & 1 deletion src/components/SecretDetailSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,10 @@
</span>
<div class="secret-detail__row-main">
<div class="secret-detail__row-value">
<PasswordField :label="keyLabel" :resolve="resolveKey" />
<PasswordField
:key="secretLoadToken"
:label="keyLabel"
:resolve="resolveKey" />
</div>
</div>
</div>
Expand Down Expand Up @@ -955,6 +958,29 @@ export default {
cardRevealed: { number: false, cvv: false, pin: false },
/** Reveal state for the identity's BSN row (masked by default). */
bsnRevealed: false,
/**
* Bumped by every successful `load()`, and bound to
* `<PasswordField :key>` so the field remounts whenever the
* loaded secret changes.
*
* PasswordField decrypts lazily and then CACHES the plaintext
* for its own lifetime (`plain` is only resolved while it is
* still `null`), and it keeps `revealed` across that lifetime
* too. That was harmless while the detail was a full page which
* remounted per secret. This sidebar deliberately does NOT
* remount — the `secretId` watcher above swaps the secret in
* place — so without a key the cache outlives the secret it
* belongs to, in two ways that both matter for a vault:
*
* • Edit the open secret: `load()` refreshes `this.secret`,
* the field keeps showing the OLD plaintext.
* • Click another row while revealed: the panel shows secret
* B's name with secret A's plaintext, and Copy copies A's.
*
* Remounting resets `plain` and `revealed` together, so a
* changed secret is re-masked until the user asks for it again.
*/
secretLoadToken: 0,
}
},

Expand Down Expand Up @@ -1340,6 +1366,9 @@ export default {
this.error = ''
try {
this.secret = await useSecretStore().fetchSecret(this.secretId)
// Retire the previous secret's decrypted plaintext with the
// secret it came from. See `secretLoadToken` in data().
this.secretLoadToken += 1
// Write-grade badge (folder-permission-grades §4.3) — a
// copy the user may team-write shows the sync warning.
try {
Expand Down
31 changes: 21 additions & 10 deletions tests/e2e/workflows/folder-sharing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@
* were credited to zero scenarios. They are anchored per-test below, against the
* `secrets-write-ui` scenarios they actually drive.
*/
import { test, expect } from '@playwright/test'
import { expect, test } from '@playwright/test'
import {
clickOverflowAction,
gotoLockSettled,
openVault,
unlockVault,
} from './_workflow-helpers'
} from './_workflow-helpers.ts'

const REQ_TOKEN = `(() => {
const head = document.querySelector('head[data-requesttoken]');
Expand Down Expand Up @@ -279,14 +279,25 @@ test.describe('Workflow: folders + sharing — folders/spec.md', () => {
await page
.getByRole('button', { name: /Secret actions/i })
.evaluate((el: HTMLElement) => el.click())
// `data-testid` falls through to NcActionButton's ROOT, which is the
// <li> wrapper, not the <button> carrying the click handler — so
// clicking the testid node itself is a silent no-op and the dialog
// never opens. Descend to the button.
await page
.getByTestId('secret-detail-move')
.locator('button')
.evaluate((el: HTMLElement) => el.click())
// TARGET THE MENUITEM, NOT A NODE GUESSED FROM THE MARKUP.
//
// This used to descend from `data-testid=secret-detail-move` to an
// inner <button> and dispatch `el.click()`, on the assumption that
// the testid lands on NcActionButton's <li> root while the handler
// sits on the button. That assumption is what broke: after the
// Stage-8 restyle the synthetic dispatch stopped reaching the
// handler, and the failure was invisible — the trace shows the
// testid RESOLVING in 0.1s and then `.move-form` timing out for 10s,
// with the menu still `[expanded]` and `menuitem "Move"` present in
// the snapshot. A click that lands on the wrong node looks exactly
// like a dialog that refuses to open.
//
// The accessibility tree exposes this as `menuitem "Move"` whatever
// element NcActionButton happens to render, so ask for that and let
// Playwright's own click do the actionability checks. It is also a
// real click rather than a dispatched event, which is what a user
// performs.
await page.getByRole('menuitem', { name: 'Move' }).click()
await expect(page.locator('.move-form')).toBeVisible({ timeout: 10_000 })
await page.locator('.move-form .vs__dropdown-toggle').click()
await page
Expand Down
Loading