mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
test(e2e): fix encryption button timing and drag-handle strict mode violation
Two root causes for failing E2E tests in CI:
1. Encryption button not visible after provider selection: The async
provider change listener loads config from IndexedDB and updates the
Formly model, but the 1000ms waitForTimeout was insufficient in CI.
Replace with polling (200ms intervals, 10s timeout) that waits for
either enable or disable encryption button to become visible.
Also removes ~130 lines of leftover debug logging from disableEncryption().
2. Drag-handle strict mode violation on parent tasks with subtasks:
task.locator('.drag-handle') matched both the parent's drag handle
and nested subtask drag handles. Add .first() to all drag-handle
locator usages to target only the parent task's own handle.
This commit is contained in:
parent
ef07ae8ff2
commit
7164a5e44b
7 changed files with 64 additions and 166 deletions
|
|
@ -907,13 +907,12 @@ export class SuperSyncPage extends BasePage {
|
|||
// Without this, the form shows default/empty values instead of the actual current state
|
||||
await this.selectSuperSyncProviderWithRetry();
|
||||
|
||||
// IMPORTANT: Wait for the provider change listener to complete loading the config
|
||||
// The listener is async and needs time to load provider config and update the form
|
||||
await this.page.waitForTimeout(1000);
|
||||
// Wait for the provider change listener to load config and update the form model.
|
||||
// The listener is async (loads from IndexedDB) so we poll for the encryption buttons
|
||||
// instead of using an arbitrary timeout that may be insufficient in CI.
|
||||
const encBtnResult = await this._waitForEncryptionButtonAfterProviderChange();
|
||||
|
||||
// Check if already enabled (button is visible at top level for SuperSync, not inside Advanced)
|
||||
const isEnabled = await this.disableEncryptionBtn.isVisible().catch(() => false);
|
||||
if (isEnabled) {
|
||||
if (encBtnResult === 'already-enabled') {
|
||||
// Already enabled - just close the dialog
|
||||
const configDialog = this.page.locator('mat-dialog-container').first();
|
||||
const cancelBtn = configDialog.locator('button').filter({ hasText: /cancel/i });
|
||||
|
|
@ -922,25 +921,7 @@ export class SuperSyncPage extends BasePage {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if the enable encryption button is visible (for SuperSync it's at the top level)
|
||||
let enableBtnVisible = await this.enableEncryptionBtn.isVisible().catch(() => false);
|
||||
|
||||
if (!enableBtnVisible) {
|
||||
// Try expanding "Advanced settings" collapsible
|
||||
const advancedCollapsible = this.page.locator(
|
||||
'.collapsible-header:has-text("Advanced")',
|
||||
);
|
||||
const advancedExists = await advancedCollapsible.isVisible().catch(() => false);
|
||||
if (advancedExists) {
|
||||
await advancedCollapsible.click();
|
||||
enableBtnVisible = await this.enableEncryptionBtn
|
||||
.waitFor({ state: 'visible', timeout: 3000 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!enableBtnVisible) {
|
||||
if (encBtnResult !== 'enable-visible') {
|
||||
throw new Error(
|
||||
'Enable encryption button not visible after checking top level and Advanced section',
|
||||
);
|
||||
|
|
@ -1008,6 +989,51 @@ export class SuperSyncPage extends BasePage {
|
|||
await this.ensureOverlaysClosed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for the encryption button state after provider selection.
|
||||
* The provider change listener asynchronously loads config from IndexedDB
|
||||
* and updates the Formly model, which controls button visibility.
|
||||
* Returns 'enable-visible', 'already-enabled', or 'not-found'.
|
||||
*/
|
||||
private async _waitForEncryptionButtonAfterProviderChange(): Promise<
|
||||
'enable-visible' | 'already-enabled' | 'not-found'
|
||||
> {
|
||||
const POLL_TIMEOUT = 10000;
|
||||
const POLL_INTERVAL = 200;
|
||||
const deadline = Date.now() + POLL_TIMEOUT;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
// Check if disable button is visible (encryption already enabled)
|
||||
if (await this.disableEncryptionBtn.isVisible().catch(() => false)) {
|
||||
return 'already-enabled';
|
||||
}
|
||||
|
||||
// Check if enable button is visible at top level (SuperSync placement)
|
||||
if (await this.enableEncryptionBtn.isVisible().catch(() => false)) {
|
||||
return 'enable-visible';
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(POLL_INTERVAL);
|
||||
}
|
||||
|
||||
// Last resort: try expanding Advanced section
|
||||
const advancedCollapsible = this.page.locator(
|
||||
'.collapsible-header:has-text("Advanced")',
|
||||
);
|
||||
if (await advancedCollapsible.isVisible().catch(() => false)) {
|
||||
await advancedCollapsible.click();
|
||||
const found = await this.enableEncryptionBtn
|
||||
.waitFor({ state: 'visible', timeout: 3000 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (found) {
|
||||
return 'enable-visible';
|
||||
}
|
||||
}
|
||||
|
||||
return 'not-found';
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the clean slate sync to complete after enabling/disabling encryption.
|
||||
*/
|
||||
|
|
@ -1038,141 +1064,11 @@ export class SuperSyncPage extends BasePage {
|
|||
// Without this, the form shows default/empty values instead of the actual current state
|
||||
await this.selectSuperSyncProviderWithRetry();
|
||||
|
||||
// IMPORTANT: Wait for the provider change listener to complete loading the config
|
||||
// The listener is async and needs time to load provider config and update the form
|
||||
await this.page.waitForTimeout(1000);
|
||||
// Wait for the provider change listener to load config and update the form model.
|
||||
// Poll for the disable encryption button which appears when isEncryptionEnabled is true.
|
||||
const encBtnResult = await this._waitForEncryptionButtonAfterProviderChange();
|
||||
|
||||
// Expand "Advanced settings" collapsible to access encryption fields
|
||||
const advancedCollapsible = this.page.locator(
|
||||
'.collapsible-header:has-text("Advanced")',
|
||||
);
|
||||
|
||||
// Debug: Check if Advanced collapsible exists
|
||||
const advancedExists = await advancedCollapsible.count();
|
||||
console.log(`[DisableEncryption] Advanced collapsible count: ${advancedExists}`);
|
||||
|
||||
// Debug: Get all collapsible headers
|
||||
const allCollapsibles = await this.page
|
||||
.locator('.collapsible-header')
|
||||
.allTextContents();
|
||||
console.log(
|
||||
`[DisableEncryption] All collapsible headers: ${JSON.stringify(allCollapsibles)}`,
|
||||
);
|
||||
|
||||
await advancedCollapsible.waitFor({ state: 'visible', timeout: 5000 });
|
||||
|
||||
// Check if already expanded
|
||||
const isExpanded = await this.disableEncryptionBtn.isVisible().catch(() => false);
|
||||
console.log(`[DisableEncryption] isExpanded before click: ${isExpanded}`);
|
||||
if (!isExpanded) {
|
||||
// Debug: Check if collapsible is clickable
|
||||
const isClickable = await advancedCollapsible.isEnabled().catch(() => false);
|
||||
console.log(`[DisableEncryption] Collapsible isEnabled: ${isClickable}`);
|
||||
|
||||
// Scroll the collapsible into view
|
||||
await advancedCollapsible.scrollIntoViewIfNeeded();
|
||||
await this.page.waitForTimeout(300);
|
||||
|
||||
// Try clicking multiple times with different methods
|
||||
console.log(`[DisableEncryption] Attempting click on collapsible...`);
|
||||
|
||||
// Method 1: Regular click with noWaitAfter to prevent navigation blocking
|
||||
await advancedCollapsible.click({ noWaitAfter: true });
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// Check if expanded
|
||||
let expandedAfterClick1 =
|
||||
(await this.page.locator('formly-collapsible.isExpanded').count()) > 0;
|
||||
console.log(`[DisableEncryption] After click 1: isExpanded=${expandedAfterClick1}`);
|
||||
|
||||
if (!expandedAfterClick1) {
|
||||
// Method 2: Click with force and noWaitAfter
|
||||
await advancedCollapsible.click({ force: true, noWaitAfter: true });
|
||||
await this.page.waitForTimeout(500);
|
||||
expandedAfterClick1 =
|
||||
(await this.page.locator('formly-collapsible.isExpanded').count()) > 0;
|
||||
console.log(
|
||||
`[DisableEncryption] After click 2 (force): isExpanded=${expandedAfterClick1}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!expandedAfterClick1) {
|
||||
// Method 3: Use JavaScript to click
|
||||
await advancedCollapsible.evaluate((el) => (el as HTMLElement).click());
|
||||
await this.page.waitForTimeout(500);
|
||||
expandedAfterClick1 =
|
||||
(await this.page.locator('formly-collapsible.isExpanded').count()) > 0;
|
||||
console.log(
|
||||
`[DisableEncryption] After click 3 (JS): isExpanded=${expandedAfterClick1}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Debug: Check collapsible state after click
|
||||
const collapsibleText = await advancedCollapsible
|
||||
.textContent()
|
||||
.catch(() => 'unknown');
|
||||
console.log(`[DisableEncryption] Collapsible text after click: ${collapsibleText}`);
|
||||
|
||||
// Debug: Check what buttons are visible in the Advanced section
|
||||
const enableBtn = await this.enableEncryptionBtn.isVisible().catch(() => false);
|
||||
const disableBtn = await this.disableEncryptionBtn.isVisible().catch(() => false);
|
||||
const changeBtn = await this.page
|
||||
.locator('button:has-text("Change Password")')
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
|
||||
// Debug: Check if the Advanced section content is visible at all
|
||||
const advancedContent = await this.page
|
||||
.locator('.collapsible-content')
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
const encryptKeyInput = await this.encryptionPasswordInput
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
|
||||
// Debug: Check the selected provider
|
||||
const selectedProvider = await this.providerSelect
|
||||
.textContent()
|
||||
.catch(() => 'unknown');
|
||||
|
||||
// Debug: Check if there are any formly fields inside the collapsible
|
||||
const formlyFieldsInCollapsible = await this.page
|
||||
.locator('.collapsible-content formly-field')
|
||||
.count();
|
||||
|
||||
// Debug: Check if there are any hidden formly fields
|
||||
const hiddenFormlyFields = await this.page
|
||||
.locator('.collapsible-content formly-field[hidden]')
|
||||
.count();
|
||||
|
||||
// Debug: Check if the collapsible panel exists (expanded state)
|
||||
const collapsiblePanel = await this.page.locator('.collapsible-panel').count();
|
||||
|
||||
// Debug: Check if formly-collapsible has isExpanded class
|
||||
const isCollapsibleExpanded = await this.page
|
||||
.locator('formly-collapsible.isExpanded')
|
||||
.count();
|
||||
|
||||
console.log('[DisableEncryption] After expand', {
|
||||
enableBtn,
|
||||
disableBtn,
|
||||
changeBtn,
|
||||
advancedContent,
|
||||
encryptKeyInput,
|
||||
selectedProvider,
|
||||
formlyFieldsInCollapsible,
|
||||
hiddenFormlyFields,
|
||||
collapsiblePanel,
|
||||
isCollapsibleExpanded,
|
||||
});
|
||||
|
||||
await this.disableEncryptionBtn.waitFor({ state: 'visible', timeout: 3000 });
|
||||
}
|
||||
|
||||
// Check if already disabled
|
||||
const isEnabled = await this.disableEncryptionBtn.isVisible().catch(() => false);
|
||||
if (isEnabled) {
|
||||
if (encBtnResult === 'already-enabled') {
|
||||
await this.disableEncryptionBtn.click();
|
||||
|
||||
// IMPORTANT: The "Disable Encryption?" confirmation dialog appears immediately
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ export class WorkViewPage extends BasePage {
|
|||
await task.waitFor({ state: 'visible' });
|
||||
|
||||
// Click the drag handle to focus the task (avoids opening edit mode on task-title)
|
||||
const dragHandle = task.locator('.drag-handle');
|
||||
// Use .first() because parent tasks contain subtask elements which also have .drag-handle
|
||||
const dragHandle = task.locator('.drag-handle').first();
|
||||
await dragHandle.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await dragHandle.click();
|
||||
await this.page.waitForTimeout(200);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ test.describe('App Features - Time Tracking', () => {
|
|||
// elements on main page
|
||||
const workViewPage = new WorkViewPage(page, testPrefix);
|
||||
const firstTask = page.locator('task').first();
|
||||
const firstTaskHandle = firstTask.locator('.drag-handle');
|
||||
const firstTaskHandle = firstTask.locator('.drag-handle').first();
|
||||
const taskPlayButton = page
|
||||
.locator('task')
|
||||
.getByRole('button', { name: 'Start tracking time' });
|
||||
|
|
|
|||
|
|
@ -645,7 +645,7 @@ test.describe('@supersync SuperSync Edge Cases', () => {
|
|||
const taskEl = clientA.page
|
||||
.locator(`task:not(.ng-animating):has-text("${taskName}")`)
|
||||
.first();
|
||||
await taskEl.locator('.drag-handle').click();
|
||||
await taskEl.locator('.drag-handle').first().click();
|
||||
await clientA.page.keyboard.press('Backspace');
|
||||
|
||||
// 4. Handle the optional confirmation dialog, then click Undo.
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ test.describe('Task Delete Confirmation', () => {
|
|||
|
||||
// Focus the task by clicking on drag-handle (not task-title which would open edit mode)
|
||||
const task = page.locator('task').first();
|
||||
const dragHandle = task.locator('.drag-handle');
|
||||
const dragHandle = task.locator('.drag-handle').first();
|
||||
await dragHandle.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await dragHandle.click();
|
||||
// Wait for focus to settle - the task component needs time to process the click
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ test.describe('Drag Task to change project and labels', () => {
|
|||
|
||||
// find drag handle of task
|
||||
const firstTask = page.locator('task').first();
|
||||
const dragHandle = firstTask.locator('.drag-handle');
|
||||
const dragHandle = firstTask.locator('.drag-handle').first();
|
||||
const tagList = firstTask.locator('tag-list');
|
||||
|
||||
// Drag and drop to first project - wait for tag to appear
|
||||
|
|
@ -131,7 +131,7 @@ test.describe('Drag Task to change project and labels', () => {
|
|||
|
||||
// find drag handle of task
|
||||
const firstTask = page.locator('task').first();
|
||||
const dragHandle = firstTask.locator('.drag-handle');
|
||||
const dragHandle = firstTask.locator('.drag-handle').first();
|
||||
const tagList = firstTask.locator('tag-list');
|
||||
|
||||
// Drag and drop to first tag - wait for tag to appear
|
||||
|
|
|
|||
|
|
@ -539,7 +539,8 @@ export const deleteTask = async (
|
|||
// Click the drag-handle to focus the task without entering title edit mode.
|
||||
// Clicking the task body can land on the task-title, opening the textarea editor,
|
||||
// which causes Backspace to delete text instead of triggering the delete shortcut.
|
||||
const dragHandle = task.locator('.drag-handle');
|
||||
// Use .first() because parent tasks contain subtask elements which also have .drag-handle
|
||||
const dragHandle = task.locator('.drag-handle').first();
|
||||
await dragHandle.click();
|
||||
await client.page.keyboard.press('Backspace');
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue