Refine local backup modal and permission flow
All checks were successful
Deploy Sokko G / deploy (push) Successful in 17s

This commit is contained in:
Shinuwa 2026-09-25 11:35:31 +02:00
parent ee823d2c37
commit 69e3b8ef2f
4 changed files with 19 additions and 12 deletions

View file

@ -1,5 +1,5 @@
// Rôle : couvre les parcours navigateur principaux des toolboxes locales. // Rôle : couvre les parcours navigateur principaux des toolboxes locales.
/* global window, navigator, CustomEvent, FileSystemDirectoryHandle */ /* global window, document, navigator, CustomEvent, FileSystemDirectoryHandle */
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@ -247,9 +247,11 @@ test("opens the local automatic backup settings modal", async ({ page }) => {
const dialog = page.getByRole("dialog", { name: "Sauvegarde automatique" }); const dialog = page.getByRole("dialog", { name: "Sauvegarde automatique" });
await expect(dialog).toBeVisible(); await expect(dialog).toBeVisible();
expect((await dialog.boundingBox()).width).toBeGreaterThan(700);
expect(await dialog.evaluate((element) => window.getComputedStyle(element).scrollbarColor)).toContain("88, 55, 174");
await expect(dialog.getByText("Cette fonctionnalité n’est pas disponible dans tous les navigateurs.")).toBeVisible(); await expect(dialog.getByText("Cette fonctionnalité n’est pas disponible dans tous les navigateurs.")).toBeVisible();
await expect(dialog.getByText("Opera GX", { exact: true })).toBeVisible(); await expect(dialog.getByText("Opera GX", { exact: true })).toBeVisible();
await expect(dialog.getByText("Fonctionnement de l’autorisation", { exact: true })).toBeVisible(); await expect(dialog.getByText("Comment fonctionne la sauvegarde", { exact: true })).toBeVisible();
await expect(dialog.getByText("la popup système ne peut apparaître qu’après un clic de votre part.")).toBeVisible(); await expect(dialog.getByText("la popup système ne peut apparaître qu’après un clic de votre part.")).toBeVisible();
await expect(dialog.getByText("10 versions sont conservées.", { exact: true })).toBeVisible(); await expect(dialog.getByText("10 versions sont conservées.", { exact: true })).toBeVisible();
await expect(dialog.getByText("Dossier configuré :", { exact: true })).toBeVisible(); await expect(dialog.getByText("Dossier configuré :", { exact: true })).toBeVisible();
@ -275,6 +277,7 @@ test("offers an actionable backup alert without requesting permission at startup
await page.addInitScript(() => { await page.addInitScript(() => {
window.backupPermissionRequests = 0; window.backupPermissionRequests = 0;
window.backupPermissionGranted = false; window.backupPermissionGranted = false;
window.backupReminderVisibleAtPermissionRequest = true;
Object.defineProperty(FileSystemDirectoryHandle.prototype, "queryPermission", { Object.defineProperty(FileSystemDirectoryHandle.prototype, "queryPermission", {
configurable: true, configurable: true,
value: async () => window.backupPermissionGranted ? "granted" : "prompt" value: async () => window.backupPermissionGranted ? "granted" : "prompt"
@ -283,6 +286,7 @@ test("offers an actionable backup alert without requesting permission at startup
configurable: true, configurable: true,
value: async () => { value: async () => {
window.backupPermissionRequests += 1; window.backupPermissionRequests += 1;
window.backupReminderVisibleAtPermissionRequest = document.body.textContent.includes("Des modifications ont été détectées dans vos toolboxes.");
window.backupPermissionGranted = true; window.backupPermissionGranted = true;
return "granted"; return "granted";
} }
@ -302,6 +306,7 @@ test("offers an actionable backup alert without requesting permission at startup
await reminder.getByRole("button", { name: "Sauvegarder", exact: true }).click(); await reminder.getByRole("button", { name: "Sauvegarder", exact: true }).click();
await expect(reminder).toHaveCount(0); await expect(reminder).toHaveCount(0);
expect(await page.evaluate(() => window.backupPermissionRequests)).toBe(1); expect(await page.evaluate(() => window.backupPermissionRequests)).toBe(1);
expect(await page.evaluate(() => window.backupReminderVisibleAtPermissionRequest)).toBe(false);
await expect.poll(async () => page.evaluate(async () => { await expect.poll(async () => page.evaluate(async () => {
const db = await import("/src/utils/indexedDbStorage.js"); const db = await import("/src/utils/indexedDbStorage.js");
return (await db.getSetting("autoBackup", {})).hasUnsavedChanges; return (await db.getSetting("autoBackup", {})).hasUnsavedChanges;

View file

@ -1816,19 +1816,20 @@
"autoBackup": { "autoBackup": {
"eyebrow": "Sauvegarde locale", "eyebrow": "Sauvegarde locale",
"title": "Sauvegarde automatique", "title": "Sauvegarde automatique",
"description": "Cette fonctionnalité n’est pas disponible dans tous les navigateurs. Les sauvegardes restent locales à ce navigateur et ne sont envoyées à aucun serveur.", "description": "La sauvegarde automatique suit les modifications confirmées de vos toolboxes et crée des fichiers JSON dans le dossier choisi. Ces fichiers restent sur votre appareil et ne sont envoyés à aucun serveur. Cette fonctionnalité n’est pas disponible dans tous les navigateurs.",
"compatibleLabel": "Navigateurs compatibles", "compatibleLabel": "Navigateurs compatibles",
"browsers": [ "browsers": [
"Opera GX", "Opera GX",
"Google Chrome", "Google Chrome",
"Microsoft Edge" "Microsoft Edge"
], ],
"permissionTitle": "Fonctionnement de l’autorisation", "permissionTitle": "Comment fonctionne la sauvegarde",
"permissionDescription": "Opera GX peut redemander l’accès après la fermeture du navigateur. Pour des raisons de sécurité, la popup système ne peut apparaître qu’après un clic de votre part.", "permissionDescription": "L’application mémorise si vos toolboxes ont changé depuis la dernière sauvegarde. Opera GX peut redemander l’accès au dossier après la fermeture du navigateur ; la popup système ne peut apparaître qu’après un clic de votre part.",
"permissionSteps": [ "permissionSteps": [
"Si l’accès est toujours accordé, la vérification et la sauvegarde éventuelle restent silencieuses.", "Après chaque modification confirmée d’une toolbox, une sauvegarde est marquée comme nécessaire.",
"L’application mémorise localement les modifications confirmées. Si l’accès a expiré, une alerte vous propose alors de les sauvegarder.", "Au prochain démarrage, si le dossier est encore accessible, sa vérification et la sauvegarde éventuelle restent silencieuses.",
"Cliquez sur « Sauvegarder » dans cette alerte : le navigateur redemande l’accès, puis l’application vérifie le dossier avant toute écriture." "Si l’accès a expiré, une alerte apparaît lorsqu’une sauvegarde est nécessaire. Son bouton « Sauvegarder » ferme immédiatement l’alerte et ouvre la demande d’autorisation d’Opera GX.",
"Après autorisation, l’application vérifie le fichier du jour et le contenu de la dernière version. Elle écrit un nouveau fichier uniquement si nécessaire."
], ],
"reminderMessage": "Des modifications ont été détectées dans vos toolboxes. Voulez-vous créer une sauvegarde locale ?", "reminderMessage": "Des modifications ont été détectées dans vos toolboxes. Voulez-vous créer une sauvegarde locale ?",
"reminderAction": "Sauvegarder", "reminderAction": "Sauvegarder",

View file

@ -1,5 +1,6 @@
// Rôle : fournit les modales globales liées aux toolboxes et confirmations. // Rôle : fournit les modales globales liées aux toolboxes et confirmations.
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { flushSync } from "react-dom";
import { Icon } from "./Icon.jsx"; import { Icon } from "./Icon.jsx";
import { lockBodyScroll } from "../utils/bodyScrollLock.js"; import { lockBodyScroll } from "../utils/bodyScrollLock.js";
@ -35,8 +36,8 @@ export function NotificationToast({ id, message, durationMs = 120000, actionLabe
<div className={`notification-toast nds-toast${actionLabel && onAction ? " has-action" : ""}`} role="status"> <div className={`notification-toast nds-toast${actionLabel && onAction ? " has-action" : ""}`} role="status">
<span>{message}</span> <span>{message}</span>
{actionLabel && onAction && <button className="nds-button notification-toast-action" data-size="xs" data-variant="primary" type="button" onClick={() => { {actionLabel && onAction && <button className="nds-button notification-toast-action" data-size="xs" data-variant="primary" type="button" onClick={() => {
flushSync(() => onClose(id));
onAction(); onAction();
onClose(id);
}}>{actionLabel}</button>} }}>{actionLabel}</button>}
<button className="nds-icon-button" data-size="xs" type="button" onClick={() => onClose(id)} aria-label="Fermer l'alerte" title="Fermer"> <button className="nds-icon-button" data-size="xs" type="button" onClick={() => onClose(id)} aria-label="Fermer l'alerte" title="Fermer">
<Icon name="close" /> <Icon name="close" />
@ -95,7 +96,7 @@ export function AutoBackupModal({ backup, content, onClose }) {
return ( return (
<div className="confirm-modal-root auto-backup-modal-root"> <div className="confirm-modal-root auto-backup-modal-root">
<div className="confirm-backdrop nds-modal-backdrop" onClick={onClose} /> <div className="confirm-backdrop nds-modal-backdrop" onClick={onClose} />
<section className="confirm-modal nds-modal auto-backup-modal" role="dialog" aria-modal="true" aria-labelledby="auto-backup-title"> <section className="confirm-modal nds-modal auto-backup-modal themed-scrollbar" role="dialog" aria-modal="true" aria-labelledby="auto-backup-title">
<header className="auto-backup-modal-header"> <header className="auto-backup-modal-header">
<div> <div>
<p className="eyebrow">{content.eyebrow}</p> <p className="eyebrow">{content.eyebrow}</p>

View file

@ -31,8 +31,8 @@
margin-bottom: var(--space-6); margin-bottom: var(--space-6);
} }
.auto-backup-modal { .auto-backup-modal.nds-modal {
width: min(620px, calc(100vw - 32px)); width: min(760px, calc(100vw - 32px));
max-height: min(760px, calc(100vh - 32px)); max-height: min(760px, calc(100vh - 32px));
overflow-y: auto; overflow-y: auto;
} }