Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export function LarkSettingsPage({
const { t } = useWorkspaceI18n();
const [tab, setTab] = useState<Tab>("connections");
const {apps, loading: appsLoading, error: appsError, refresh: refreshApps} = useLarkApps();
const appCount = appsLoading || appsError !== null ? "…" : apps.length;
const [connections, setConnections] = useState<LarkGoalConnection[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -554,7 +555,7 @@ export function LarkSettingsPage({
)}

<nav className="personal-lark-tabs" aria-label={t("lark.management")}>
<button aria-current={tab === "apps" ? "page" : undefined} onClick={() => setTab("apps")} type="button">{t("lark.apps")} <span>{appsLoading ? "…" : apps.length}</span></button>
<button aria-current={tab === "apps" ? "page" : undefined} onClick={() => setTab("apps")} type="button">{t("lark.apps")} <span>{appCount}</span></button>
<button aria-current={tab === "connections" ? "page" : undefined} onClick={() => setTab("connections")} type="button">{t("lark.connections")} <span>{loading ? "…" : connections.length}</span></button>
</nav>
{tab === "connections" ? <PrivateConversationPanel apps={apps.filter(app => app.ready && app.app_ref !== "default")} onOpenOriginal={onOpenOriginalConversation} /> : null}
Expand All @@ -564,7 +565,7 @@ export function LarkSettingsPage({

{tab === "apps" ? (
<div className="personal-lark-apps">
<div className="personal-lark-app-toolbar"><span>{t("lark.reusableApps", { count: apps.length })}</span><div className="personal-lark-app-actions"><button className="personal-icon-button" title={t("lark.retryVerification")} aria-label={t("lark.retryVerification")} disabled={appsLoading} onClick={() => void refreshApps()} type="button"><RefreshCw size={16} /></button><button className="personal-primary-action" disabled={cliUnavailable || larkCliUnavailable(appsError)} onClick={openSetup} type="button"><Plus size={16} />{t("lark.newApp")}</button></div></div>
<div className="personal-lark-app-toolbar"><span>{t("lark.reusableApps", { count: appCount })}</span><div className="personal-lark-app-actions"><button className="personal-icon-button" title={t("lark.retryVerification")} aria-label={t("lark.retryVerification")} disabled={appsLoading} onClick={() => void refreshApps()} type="button"><RefreshCw size={16} /></button><button className="personal-primary-action" disabled={cliUnavailable || larkCliUnavailable(appsError)} onClick={openSetup} type="button"><Plus size={16} />{t("lark.newApp")}</button></div></div>
<div className="personal-lark-app-grid">
{apps.map((app) => (
<article className="personal-lark-app-card" key={app.app_ref}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,8 @@ assert.match(larkSettings, /fetchLarkAppSetup/, "Registration polls the local se
assert.match(larkSettings, /window\.open\(/, "Registration opens the provider flow from a user gesture");
assert.match(larkSettings, /window\.open\(window\.location\.href,\s*"_blank"\)/, "Registration never leaves a blank waiting tab");
assert.match(larkSettings, /setAppRef\(snapshot\.app_ref\)/, "Completed registration selects the new App");
assert.match(larkSettings, /loading \? "…" : apps\.length/, "Lark App count does not flash a false zero while loading");
// Loading/empty App counts are exercised against the rendered page in the
// progressive-loading browser scenario, independently of local state names.
assert.match(larkSettings, /openConnectionEditor\(connection\)/, "Every Lark connection settings button opens a scoped editor");
assert.match(larkSettings, /focusGoalConnection[\s\S]*openConnectionEditor\(connection\)[\s\S]*openConnect\(goals\.find/, "Goal-level Lark entry opens the scoped connection editor or create flow");

Expand Down
45 changes: 42 additions & 3 deletions examples/personal-workspace-browser/progressive-loading.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {resolve} from "node:path";
import {outputDir} from "./fixture.mjs";

async function verifyLarkLoading(browser, url, collectCoverage) {
let releaseCatalog;
const catalog = new Promise(resolve => {releaseCatalog = resolve;});
let releaseSlow;
const slow = new Promise(resolve => {releaseSlow = resolve;});
let revision = 1, catalogs = 0;
Expand All @@ -11,8 +13,14 @@ async function verifyLarkLoading(browser, url, collectCoverage) {
await page.route("**/api/chat/lark/apps?*", async route => {
const query = new URL(route.request().url()).searchParams;
const current = revision;
const refs = ["fast", "slow", "failed"];
if (query.get("verify") === "false") catalogs++;
const refs = current === 3 ? [] : ["fast", "slow", "failed"];
if (query.get("verify") === "false") {
catalogs++;
if (current === 1) await catalog;
if (current === 4) {
await route.fulfill({status: 502, json: {ok: false, error: "Synthetic catalog outage", error_code: "lark_configuration_unavailable"}}); return;
}
}
else {
probes.push(query.get("app_ref"));
if (query.get("app_ref") === "slow" && current === 1) await slow;
Expand All @@ -38,6 +46,19 @@ async function verifyLarkLoading(browser, url, collectCoverage) {
}
try {
await openSettings();
const appTab = page.locator(".personal-lark-tabs").getByRole("button", {name: /^Lark Apps/u});
// Hold the actual catalog response: an unknown count must not masquerade
// as an empty directory, regardless of the component's state variable names.
await appTab.getByText("…", {exact: true}).waitFor();
await page.screenshot({path: resolve(outputDir, "lark-catalog-pending-desktop.png"), animations: "disabled"});
if (await appTab.getByText("0", {exact: true}).count() || await page.locator(".personal-lark-empty").count()) throw new Error("Pending catalog displayed a false empty directory");
if ((await page.locator(".personal-lark-app-toolbar > span").innerText()) !== "… 个可复用 App") throw new Error("Pending catalog toolbar displayed a false App count");
await page.setViewportSize({width: 390, height: 844});
if (await page.evaluate(() => document.documentElement.scrollWidth > innerWidth)) throw new Error("Pending Lark catalog overflows at 390px");
await page.screenshot({path: resolve(outputDir, "lark-catalog-pending-narrow.png"), animations: "disabled"});
await page.setViewportSize({width: 1512, height: 982});
releaseCatalog();
await appTab.getByText("3", {exact: true}).waitFor();
const fast = page.locator(".personal-lark-app-card").filter({hasText: "fast 1"});
await fast.getByText("自动回复可用", {exact: true}).waitFor();
await page.locator(".personal-lark-app-card").filter({hasText: "slow 1"}).getByText("正在核验…", {exact: true}).waitFor();
Expand All @@ -52,12 +73,30 @@ async function verifyLarkLoading(browser, url, collectCoverage) {
releaseSlow();
await page.waitForTimeout(200);
if (catalogs !== 2 || probes.length !== 6 || await page.getByText("slow 1", {exact: true}).count()) throw new Error("Late verification escaped its settings generation");
// A confirmed empty catalog is different from loading. It clears the old
// cards, schedules no health probes and can recover on the same page.
revision = 3;
await page.getByRole("button", {name: "刷新配置并重试核验", exact: true}).click();
await appTab.getByText("0", {exact: true}).waitFor();
await page.locator(".personal-lark-empty").waitFor();
if (await page.locator(".personal-lark-app-card").count() || catalogs !== 3 || probes.length !== 6) throw new Error("Empty catalog retained stale profiles or started verification");
// A failed read is unknown, never a confirmed zero; its visible error and
// retry stay available without retaining the previous directory's cards.
revision = 4;
await page.getByRole("button", {name: "刷新配置并重试核验", exact: true}).click();
await page.locator(".personal-notification-error").waitFor();
await page.screenshot({path: resolve(outputDir, "lark-catalog-failed.png"), animations: "disabled"});
if (await appTab.getByText("…", {exact: true}).count() !== 1 || (await page.locator(".personal-lark-app-toolbar > span").innerText()) !== "… 个可复用 App" || await page.locator(".personal-lark-empty, .personal-lark-app-card").count() || catalogs !== 4 || probes.length !== 6) throw new Error("Failed catalog masqueraded as an empty or stale directory");
revision = 5;
await page.getByRole("button", {name: "刷新配置并重试核验", exact: true}).click();
await page.locator(".personal-lark-app-card").filter({hasText: "slow 5"}).getByText("自动回复可用", {exact: true}).waitFor();
if (catalogs !== 5 || probes.length !== 9 || await page.locator(".personal-lark-empty, .personal-notification-error").count()) throw new Error("Failed catalog did not recover without duplicate reads");
await page.setViewportSize({width: 390, height: 844});
if (await page.evaluate(() => document.documentElement.scrollWidth > innerWidth)) throw new Error("Lark configuration overflows at 390px");
await page.screenshot({path: resolve(outputDir, "lark-progressive-narrow.png"), animations: "disabled"});
if (context.errors.some(error => !error.startsWith("Failed to load resource:"))) throw new Error(context.errors.join(" | "));
return await context.close();
} catch (error) {releaseSlow(); await context.close(); throw error;}
} catch (error) {releaseCatalog(); releaseSlow(); await context.close(); throw error;}
}

// One Goal's lifecycle action must not send the rest of the workspace back to
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading