From b2bcb0f4db47151ec3d5d778db979804e4dcc336 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Sat, 19 Sep 2026 06:57:27 +0300 Subject: [PATCH] Make workspace loading a calm progress-only screen (#29) * Make workspace loading a calm progress-only screen * Cover quiet workspace loading and error recovery on desktop and mobile * Run workspace loading regressions in desktop, mobile and WebKit QA --- public/core/login-signature-v1776.css | 13 +++++ tests/playwright.config.mjs | 4 +- tests/workspace-loading.spec.mjs | 72 +++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 tests/workspace-loading.spec.mjs diff --git a/public/core/login-signature-v1776.css b/public/core/login-signature-v1776.css index 8863a8b..9d10e7b 100644 --- a/public/core/login-signature-v1776.css +++ b/public/core/login-signature-v1776.css @@ -18,3 +18,16 @@ body.sun-cloud-auth-required{overflow:hidden!important;background:#f5f0e7!import #sunCloudAuthGateV3 .sun-auth-eye{position:absolute;border:0;cursor:pointer;font-size:17px} #sunCloudAuthGateV3 .sun-auth-switch button{cursor:pointer} + +/* Waiting for workspace access is normal progress, not a recovery screen. + Keep these rules in the synchronously loaded stylesheet to avoid a button + flash even when the optional login decoration script is delayed or blocked. + Error, missing-company and signed-out states keep their existing controls. */ +#sunCloudAuthGateV3[data-auth-state="loading"] .sun-cloud-auth-actions, +#sunCloudAuthGateV3[data-auth-state="loading"] .sun-cloud-auth-card>p.hint, +#sunCloudAuthGateV3[data-auth-state="loading"] .sun-cloud-auth-brand .hint, +#sunCloudAuthGateV3[data-auth-state="loading"] .sun-cloud-auth-error, +#sunCloudAuthGateV3[data-auth-state="loading"] .ct-help-login{display:none!important} +#sunCloudAuthGateV3[data-auth-state="loading"] .sun-cloud-auth-brand:after{content:'';display:block;width:28px;height:28px;margin-top:30px;border:3px solid rgba(211,164,60,.2);border-top-color:#d3a43c;border-radius:50%;animation:caterium-workspace-loading .9s linear infinite} +@keyframes caterium-workspace-loading{to{transform:rotate(360deg)}} +@media(prefers-reduced-motion:reduce){#sunCloudAuthGateV3[data-auth-state="loading"] .sun-cloud-auth-brand:after{animation:none}} diff --git a/tests/playwright.config.mjs b/tests/playwright.config.mjs index f2865c5..9c12877 100644 --- a/tests/playwright.config.mjs +++ b/tests/playwright.config.mjs @@ -2,13 +2,13 @@ import { defineConfig, devices } from '@playwright/test'; import {fileURLToPath} from 'node:url'; export default defineConfig({ testDir:'.', - testMatch:['ui-stability.spec.mjs','help-center.spec.mjs','app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','trial-demo.spec.mjs','proposal-quality.spec.mjs'], + testMatch:['ui-stability.spec.mjs','help-center.spec.mjs','app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','workspace-loading.spec.mjs','trial-demo.spec.mjs','proposal-quality.spec.mjs'], timeout:30000, use:{baseURL:'http://127.0.0.1:4173'}, webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true}, projects:[ {name:'iphone-pdf',testMatch:['proposal-quality.spec.mjs'],grep:/all six selections|transparent wide|an actual offer downloads/,use:{...devices['iPhone 13'],serviceWorkers:'block'}}, - {name:'iphone-webkit',testMatch:['ui-stability.spec.mjs','help-center.spec.mjs','login-recovery.spec.mjs','account-access.spec.mjs','calendar-print.spec.mjs'],use:{...devices['iPhone 13'],serviceWorkers:'block'}}, + {name:'iphone-webkit',testMatch:['ui-stability.spec.mjs','help-center.spec.mjs','login-recovery.spec.mjs','workspace-loading.spec.mjs','account-access.spec.mjs','calendar-print.spec.mjs'],use:{...devices['iPhone 13'],serviceWorkers:'block'}}, {name:'desktop',use:{...devices['Desktop Chrome']}}, {name:'mobile-390',use:{viewport:{width:390,height:844},isMobile:true,hasTouch:true}} ] diff --git a/tests/workspace-loading.spec.mjs b/tests/workspace-loading.spec.mjs new file mode 100644 index 0000000..cb4c64a --- /dev/null +++ b/tests/workspace-loading.spec.mjs @@ -0,0 +1,72 @@ +import {test,expect} from '@playwright/test'; + +const gateSelector='#sunCloudAuthGateV3'; + +async function startWorkspaceLookup(page,{withoutDecoration=false}={}){ + await page.route('https://**',route=>route.abort()); + if(withoutDecoration)await page.route('**/core/login-signature-v1776.js*',route=>route.abort()); + await page.addInitScript(()=>{ + localStorage.setItem('sunCloudV2Config',JSON.stringify({workspaceId:'startup-company',localWorkspaceId:'startup-company',tenantStorageReady:true,autoSync:false})); + window.workspaceLookupCount=0; + window.supabase={createClient:()=>({ + auth:{onAuthStateChange:()=>({data:{subscription:{unsubscribe(){}}}}),getSession:()=>new Promise(resolve=>{window.finishSessionRestore=resolve}),getUser:async()=>({data:{user:null}})}, + rpc:async name=>{ + if(name==='sun_my_workspaces'){ + window.workspaceLookupCount++; + return new Promise(resolve=>{window.finishWorkspaceLookup=resolve}); + } + return {data:null,error:null}; + }, + channel:()=>({on(){return this},subscribe(){return this}}),removeChannel:()=>{} + })}; + }); + await page.goto('/index.html',{waitUntil:'domcontentloaded'}); + await page.waitForFunction(()=>Boolean(window.finishSessionRestore)); + await page.evaluate(()=>finishSessionRestore({data:{session:{user:{id:'startup-user',email:'test@example.invalid'}}},error:null})); + await page.waitForFunction(()=>window.workspaceLookupCount===1); + await expect(page.locator(gateSelector)).toHaveAttribute('data-auth-state','loading'); +} + +async function expectQuietLoading(page){ + const gate=page.locator(gateSelector); + await expect(gate.locator('h2')).toHaveText('Загружаю рабочую базу'); + await expect(gate.locator('button:visible,input:visible,a:visible')).toHaveCount(0); + await expect(gate.locator('.sun-cloud-auth-actions')).toBeHidden(); + await expect(gate.locator('.sun-cloud-auth-error')).toBeHidden(); + await expect(gate.locator('.hint:visible')).toHaveCount(0); + await expect(page.locator('body > header')).toBeHidden(); + expect(await gate.locator('.sun-cloud-auth-brand').evaluate(el=>getComputedStyle(el,'::after').animationName)).toBe('caterium-workspace-loading'); +} + +async function resolveWorkspace(page){ + await page.evaluate(()=>finishWorkspaceLookup({data:[{id:'startup-company',name:'Test company',role:'admin',is_active:true}],error:null})); + await expect(page.locator(gateSelector)).toHaveCount(0); + await expect(page.locator('body > header')).toBeVisible(); +} + +for(const withoutDecoration of [false,true]){ + test(`workspace loading has no recovery controls and opens automatically (decoration blocked: ${withoutDecoration})`,async({page})=>{ + await startWorkspaceLookup(page,{withoutDecoration}); + await expectQuietLoading(page); + await page.emulateMedia({reducedMotion:'reduce'}); + expect(await page.locator(`${gateSelector} .sun-cloud-auth-brand`).evaluate(el=>getComputedStyle(el,'::after').animationName)).toBe('none'); + await resolveWorkspace(page); + }); +} + +test('failed workspace lookup retains recovery controls and retry returns to quiet loading',async({page})=>{ + await startWorkspaceLookup(page); + await expectQuietLoading(page); + await page.evaluate(()=>finishWorkspaceLookup({data:null,error:{message:'Workspace lookup failed',code:'TEST_NETWORK'}})); + const gate=page.locator(gateSelector); + await expect(gate).toHaveAttribute('data-auth-state','error'); + await expect(page.locator('#sunGateRetryWorkspaceV3')).toBeVisible(); + await expect(page.locator('#sunGateSignOutV3')).toBeVisible(); + await expect(page.locator('#sunGateErrorV3')).toBeVisible(); + expect(await gate.locator('.sun-cloud-auth-brand').evaluate(el=>getComputedStyle(el,'::after').content)).toBe('none'); + await page.locator('#sunGateRetryWorkspaceV3').click(); + await page.waitForFunction(()=>window.workspaceLookupCount===2); + await expect(gate).toHaveAttribute('data-auth-state','loading'); + await expectQuietLoading(page); + await resolveWorkspace(page); +});