mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-21 02:20:12 +00:00
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
188 lines
6.8 KiB
TypeScript
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);
|
|
}
|
|
}
|