super-productivity/e2e/pages/import.page.ts
Johannes Millan 2f8d871042 feat(shepherd): remove all tours except keyboard navigation
Remove the full welcome tour (Welcome, AddTask, DeleteTask, Projects,
Sync, IssueProviders, ProductivityHelper, FinalCongrats, StartTourAgain)
and keep only the KeyboardNav tour, triggered from the Help menu.

- Delete ShepherdComponent (auto-start on load)
- Strip shepherd-steps.const.ts to KeyboardNav steps only
- Simplify ShepherdService with _initPromise guard and fresh tour on re-open
- Remove 3 tour menu items from Help menu, keep keyboard tour
- Remove waitForEl/waitForElObs$ helpers (unused by KeyboardNav)
- Delete e2e/utils/tour-helpers.ts and all tour dismissal calls
- Clean up stale tour comments across e2e tests
- Fix pre-existing build error: add isFinishDayEnabled to AppFeaturesConfig
  type, defaults, form toggle, component signal, and translations
2026-03-22 17:00:42 +01:00

188 lines
6.8 KiB
TypeScript

import { type Page, type Locator } from '@playwright/test';
import { BasePage } from './base.page';
import * as path from 'path';
/**
* Page object for file import operations.
* Handles navigation to import settings and file import functionality.
*/
export class ImportPage extends BasePage {
/** Import from file button */
readonly importFromFileBtn: Locator;
/** Hidden file input element */
readonly fileInput: Locator;
/** Import from URL button */
readonly importFromUrlBtn: Locator;
/** Export backup button */
readonly exportBackupBtn: Locator;
constructor(page: Page) {
super(page);
// The import button contains text "Import from file" and has file_download icon
this.importFromFileBtn = page.locator(
'file-imex button:has(mat-icon:has-text("file_download"))',
);
this.fileInput = page.locator('file-imex input[type="file"]');
this.importFromUrlBtn = page.locator(
'file-imex button:has(mat-icon:has-text("cloud_download"))',
);
this.exportBackupBtn = page.locator(
'file-imex button:has(mat-icon:has-text("file_upload"))',
);
}
/**
* Navigate to the import/export settings page.
*/
async navigateToImportPage(): Promise<void> {
await this.page.goto('/#/config');
await this.page.waitForLoadState('networkidle');
// Wait for page content to fully render
await this.page.waitForTimeout(1000);
// The file-imex component is now in the "Sync & Backup" tab (5th tab, index 4)
// Step 1: Click on the "Sync & Backup" tab to navigate to it
const syncBackupTab = this.page.locator(
'mat-tab-header .mat-mdc-tab:has(mat-icon:has-text("cloud_sync"))',
);
await syncBackupTab.waitFor({ state: 'visible', timeout: 10000 });
// Ensure tab is scrolled into view and stable before clicking
await syncBackupTab.scrollIntoViewIfNeeded();
await this.page.waitForTimeout(200);
await syncBackupTab.click();
// Verify the tab became active by checking for aria-selected attribute
// Retry click if tab didn't become active
const maxTabRetries = 3;
for (let i = 0; i < maxTabRetries; i++) {
const isActive = await syncBackupTab
.getAttribute('aria-selected')
.then((val) => val === 'true')
.catch(() => false);
if (isActive) {
break;
}
if (i < maxTabRetries - 1) {
console.log(
`[ImportPage] Tab click didn't register, retrying (attempt ${i + 2})`,
);
await this.page.waitForTimeout(300);
await syncBackupTab.click();
}
}
// Wait for tab panel content to load
await this.page.waitForTimeout(500);
// Step 2: Within the tab, expand the "Import/Export" collapsible section
const importExportSection = this.page.locator(
'collapsible:has-text("Import/Export")',
);
await importExportSection.waitFor({ state: 'visible', timeout: 10000 });
await importExportSection.scrollIntoViewIfNeeded();
await this.page.waitForTimeout(300);
// Click on the collapsible header to expand it with retry logic
const collapsibleHeader = importExportSection.locator('.collapsible-header, .header');
await collapsibleHeader.click();
// Verify expansion by checking if import button becomes visible
// Retry if expansion failed
const maxExpandRetries = 3;
for (let i = 0; i < maxExpandRetries; i++) {
const isExpanded = await this.importFromFileBtn
.waitFor({ state: 'visible', timeout: 2000 })
.then(() => true)
.catch(() => false);
if (isExpanded) {
break;
}
if (i < maxExpandRetries - 1) {
console.log(
`[ImportPage] Collapsible expansion failed, retrying (attempt ${i + 2})`,
);
await this.page.waitForTimeout(300);
await collapsibleHeader.click();
}
}
// Now the file-imex component should be visible in the active tab
await this.importFromFileBtn.waitFor({ state: 'visible', timeout: 10000 });
}
/**
* Import a backup file using the file input.
* This triggers the native file dialog via Playwright's setInputFiles.
*
* @param filePath - Absolute path to the backup JSON file
*/
async importBackupFile(filePath: string): Promise<void> {
// Start listening for the import completion signal BEFORE triggering the import.
// The "[SP_ALL] Load(import) all data" console message fires when
// BackupService.importCompleteBackup() has persisted to IndexedDB and dispatched to NgRx.
// We must start listening first because handleFileInput() reads the file asynchronously
// via FileReader, and the import may complete before we start listening.
const importCompletePromise = this.page.waitForEvent('console', {
predicate: (msg) => msg.text().includes('Load(import) all data'),
timeout: 60000,
});
// Set the file on the hidden input element.
// setInputFiles() triggers the (change) event on the input, which starts the import.
// Do NOT dispatch an additional change event — that would trigger the import twice.
await this.fileInput.setInputFiles(filePath);
// Handle encryption warning dialog if it appears.
// The dialog may appear several seconds after setInputFiles because FileReader is async.
// Race: either the dialog appears (handle it) or the import completes without it.
const encryptionWarning = this.page
.locator('dialog-import-encryption-warning')
.first();
const dialogOrImport = await Promise.race([
encryptionWarning
.waitFor({ state: 'visible', timeout: 15000 })
.then(() => 'dialog' as const),
importCompletePromise.then(() => 'import_complete' as const),
]);
if (dialogOrImport === 'dialog') {
console.log('[ImportPage] Encryption warning dialog appeared - confirming import');
const confirmBtn = encryptionWarning.locator('button[color="warn"]');
await confirmBtn.click();
await encryptionWarning.waitFor({ state: 'hidden', timeout: 10000 });
// Now wait for the import to actually complete
await importCompletePromise;
}
// If 'import_complete', the import finished without showing the dialog
// Wait for Angular router navigation to TODAY tag (happens right after dispatch)
const startTime = Date.now();
const timeout = 10000;
while (Date.now() - startTime < timeout) {
const url = this.page.url();
if (url.includes('tag') && url.includes('TODAY')) {
break;
}
await this.page.waitForTimeout(200);
}
await this.page.waitForLoadState('networkidle');
await this.page.waitForTimeout(500);
}
/**
* Get the path to a test fixture file.
*
* @param filename - Name of the fixture file in e2e/fixtures/
* @returns Absolute path to the fixture file
*/
static getFixturePath(filename: string): string {
return path.resolve(__dirname, '..', 'fixtures', filename);
}
}