Files
twitter-lite/e2e/integrations/deck.test.ts
T

309 lines
12 KiB
TypeScript

import { expect, test } from "../fixtures";
import { DeckPage } from "../models/DeckPage";
test.beforeEach(async ({ deckPage, workspacePage }) => {
await deckPage.goTo();
await expect(deckPage.saveAsDeck).toBeEnabled();
await deckPage.saveAsDeck.click();
await workspacePage.showDeckManagement();
await expect(deckPage.savedNotice).toBeVisible();
await workspacePage.closeMobileSidebar();
});
test("binds identical searches to independent profiles through paging, editing and reload", async ({
page,
deckPage,
}) => {
await deckPage.addColumn("通常の接続", "same-query");
await deckPage.addColumn("別の接続", "same-query", "alternate");
await expect(deckPage.columnPostText("通常の接続", "Latest · all page 1", true)).toBeVisible();
await expect(
deckPage.columnPostText("別の接続", "alternate · Latest · all page 1", true),
).toBeVisible();
await deckPage.loadMore("通常の接続");
await expect(deckPage.columnPosts("通常の接続")).toHaveCount(2);
await expect(deckPage.columnPosts("別の接続")).toHaveCount(1);
await deckPage.loadMore("別の接続");
await expect(
deckPage.columnPostText("別の接続", "alternate · Latest · all page 2"),
).toBeVisible();
await deckPage.openColumnActions("別の接続");
await deckPage.editColumn.click();
await deckPage.editorProfile.selectOption({ label: "e2e" });
await deckPage.saveChanges.click();
await expect(deckPage.columnPostText("別の接続", "Latest · all page 1", true)).toBeVisible();
await expect(deckPage.columnPostText("別の接続", "alternate · Latest · all page 1")).toHaveCount(
0,
);
await page.reload();
await expect(deckPage.columnPostText("別の接続", "Latest · all page 1", true)).toBeVisible();
await expect(deckPage.columnPosts("通常の接続")).toHaveCount(1);
});
test("creates, switches, renames and deletes persisted deck profiles", async ({
page,
deckPage,
workspacePage,
}) => {
await deckPage.rename.click();
await deckPage.deckName.fill("最初の調査");
await deckPage.saveName.click();
await deckPage.addColumn("最初の観点", "first-deck");
await workspacePage.showDeckManagement();
await deckPage.createDeck.click();
await deckPage.newDeckName.fill("別の調査");
await deckPage.confirmCreate.click();
await expect(deckPage.columns).toHaveCount(0);
await deckPage.addColumn("別の観点", "second-deck", "alternate");
await workspacePage.showDeckManagement();
await deckPage.deckProfile.selectOption({ label: "最初の調査" });
await workspacePage.closeMobileSidebar();
await expect(deckPage.columnTitles).toHaveText(["最初の観点"]);
await page.reload();
await expect(deckPage.heading).toHaveText("最初の調査");
await workspacePage.showDeckManagement();
await deckPage.deckProfile.selectOption({ label: "別の調査" });
await workspacePage.closeMobileSidebar();
await expect(deckPage.columnTitles).toHaveText(["別の観点"]);
await workspacePage.showDeckManagement();
await deckPage.deleteDeck.click();
await deckPage.confirmDelete.click();
await page.reload();
await expect(deckPage.heading).toHaveText("最初の調査");
await workspacePage.showDeckManagement();
await expect(deckPage.profileOptions).toHaveCount(1);
});
test("reads user and list timelines within columns and reorders, deletes and restores them", async ({
page,
deckPage,
}) => {
await deckPage.addColumn("アカウント", "yuta", "alternate", "user");
await deckPage.addColumn("リストの投稿", "1234567890", "e2e", "list");
await expect(deckPage.columnPostText("アカウント", "alternate profile page 1")).toBeVisible();
await expect(deckPage.columnPostText("リストの投稿", "list page 1")).toBeVisible();
await deckPage.loadMore("リストの投稿");
await expect(deckPage.columnPostText("リストの投稿", "list page 2")).toBeVisible();
await deckPage.openColumnActions("リストの投稿");
await deckPage.columnAction("Move リストの投稿 left").click();
await expect(deckPage.columnTitles).toHaveText(["リストの投稿", "アカウント"]);
await deckPage.openColumnActions("リストの投稿");
await deckPage.columnAction("Delete リストの投稿").click();
await deckPage.undo.click();
await expect(deckPage.columnTitles).toHaveText(["リストの投稿", "アカウント"]);
await page.reload();
await expect(deckPage.columnPostText("アカウント", "alternate profile page 1")).toBeVisible();
await expect(deckPage.columnPostText("リストの投稿", "list page 1")).toBeVisible();
});
test("keeps other columns usable when pagination fails and retries that column", async ({
deckPage,
}, testInfo) => {
await deckPage.addColumn("失敗する検索", `retry-deck-${testInfo.project.name}`);
await deckPage.addColumn("別の検索", "independent-deck");
await deckPage.loadMore("失敗する検索");
await expect(deckPage.columnError("失敗する検索")).toBeVisible();
await expect(deckPage.columnPosts("失敗する検索")).toHaveCount(1);
await deckPage.loadMore("別の検索");
await expect(deckPage.columnPosts("別の検索")).toHaveCount(2);
await deckPage.retryColumn("失敗する検索");
await expect(deckPage.columnPosts("失敗する検索")).toHaveCount(2);
});
test("keeps the deck and editor accessible within narrow viewports", async ({
page,
deckPage,
workspacePage,
a11y,
}, testInfo) => {
await deckPage.addColumn("日本語の反応", "WebMCP lang:ja");
await deckPage.addColumn("海外の反応", "WebMCP lang:en", "alternate");
await deckPage.addColumn("公式アカウント", "OpenAI", "e2e", "user");
for (const width of [320, 375, 414, 768]) {
await page.setViewportSize({ width, height: 900 });
expect(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth)).toBe(
false,
);
expect(await page.evaluate(() => document.documentElement.scrollHeight <= innerHeight)).toBe(
true,
);
const board = await deckPage.board.boundingBox();
expect(board).not.toBeNull();
expect((board?.y ?? 0) + (board?.height ?? 0)).toBeLessThanOrEqual(901);
}
await page.setViewportSize({
width: testInfo.project.name === "desktop" ? 1440 : 393,
height: 900,
});
await expect(deckPage.columnNavigation).toHaveCount(0);
await expect(deckPage.columnHeading("公式アカウント")).toBeVisible();
expect((await a11y().analyze()).violations).toEqual([]);
await page.screenshot({
path: testInfo.outputPath("deck-populated.png"),
fullPage: true,
});
await workspacePage.showDeckManagement();
await deckPage.addColumnButton.click();
expect((await a11y().analyze()).violations).toEqual([]);
await page.screenshot({
path: testInfo.outputPath("deck.png"),
fullPage: true,
});
});
test("closes the column editor with Escape and restores focus without applying edits", async ({
page,
deckPage,
workspacePage,
}) => {
await workspacePage.showDeckManagement();
const opener = deckPage.addColumnButton;
await opener.click();
await expect(deckPage.columnDialog).toBeVisible();
await deckPage.columnName.fill("キャンセルする観点");
await page.keyboard.press("Escape");
await expect(deckPage.columnDialog).toHaveCount(0);
await expect(opener).toBeFocused();
await expect(deckPage.columns).toHaveCount(0);
await opener.click();
await expect(deckPage.columnName).toHaveValue("");
});
test("does not overwrite unreadable storage on reload", async ({ page, deckPage }) => {
await page.evaluate(() => localStorage.setItem("twitter-lite-research-deck", "{broken"));
await page.reload();
await expect(deckPage.alert).toContainText("Unable to load");
expect(await page.evaluate(() => localStorage.getItem("twitter-lite-research-deck"))).toBe(
"{broken",
);
});
test("selects a list from the column profile catalog and refreshes it on profile change", async ({
deckPage,
workspacePage,
}) => {
await workspacePage.showDeckManagement();
await deckPage.addColumnButton.click();
await deckPage.columnName.fill("購読リスト");
await deckPage.editorProfile.selectOption({ label: "e2e" });
await deckPage.columnType.selectOption("list");
await expect(deckPage.primaryList).toBeVisible();
await deckPage.editorProfile.selectOption({ label: "alternate" });
await expect(deckPage.primaryList).toHaveCount(0);
await deckPage.alternateList.click();
await expect(deckPage.listId).toHaveValue("1234567890");
await deckPage.addAndSearch.click();
await workspacePage.closeMobileSidebar();
await expect(deckPage.columnPostText("購読リスト", "list page 1", true)).toBeVisible();
});
test("keeps an open draft through remote edits and rejects its stale save", async ({
page,
deckPage,
browser,
baseURL,
}) => {
const other = await browser.newContext({
storageState: await page.context().storageState(),
baseURL,
extraHTTPHeaders: { "Tailscale-User-Login": "[email protected]" },
});
try {
const device = await other.newPage();
const deviceDeck = new DeckPage(device);
await deviceDeck.goTo();
await deckPage.rename.click();
await deckPage.deckName.fill("編集中の名前");
await deviceDeck.rename.click();
await deviceDeck.deckName.fill("別の端末の変更");
await deviceDeck.saveName.click();
await expect(deviceDeck.heading).toHaveText("別の端末の変更");
await page.evaluate(() => window.dispatchEvent(new Event("focus")));
await expect(deckPage.deckName).toHaveValue("編集中の名前");
await deckPage.saveName.click();
await expect(deckPage.alert).toContainText("This deck changed on another device");
await expect(deckPage.deckName).toHaveValue("編集中の名前");
await page.keyboard.press("Escape");
await deckPage.reloadLatest.click();
await expect(deckPage.heading).toHaveText("別の端末の変更");
} finally {
await other.close();
}
});
test("imports old browser decks explicitly and avoids duplicate imports after retry", async ({
page,
deckPage,
workspacePage,
}) => {
const legacy = {
version: 2,
activeDeckId: "old",
decks: [
{
id: "old",
title: "以前の調査",
columns: [
{
id: "a",
title: "以前の観点",
profileName: "e2e",
source: {
platform: "twitter",
kind: "search",
query: "WebMCP",
product: "Latest",
following: false,
},
},
],
},
],
};
await page.evaluate(
(legacy) => localStorage.setItem("twitter-lite-research-deck", JSON.stringify(legacy)),
legacy,
);
await page.reload();
await workspacePage.showDeckManagement();
const selector = deckPage.deckProfile;
await expect(deckPage.profileOptions).toHaveCount(1);
await workspacePage.closeMobileSidebar();
await deckPage.importLegacy.click();
await workspacePage.showDeckManagement();
await expect(deckPage.profileOptions).toHaveCount(2);
await selector.selectOption({ label: "以前の調査" });
await workspacePage.closeMobileSidebar();
await expect(deckPage.postText("Latest · all page 1")).toBeVisible();
await page.evaluate(
(legacy) => localStorage.setItem("twitter-lite-research-deck", JSON.stringify(legacy)),
legacy,
);
await page.reload();
await deckPage.importLegacy.click();
await expect(deckPage.importLegacy).toHaveCount(0);
await workspacePage.showDeckManagement();
await expect(deckPage.profileOptions).toHaveCount(2);
});
test("preserves a new deck dialog and its draft across the mobile breakpoint", async ({
page,
deckPage,
workspacePage,
}) => {
await workspacePage.showDeckManagement();
await deckPage.createDeck.click();
const dialog = deckPage.createDialog;
await deckPage.newDeckName.fill("画面を回転しても残る調査");
for (const width of [1024, 375, 1024]) {
await page.setViewportSize({ width, height: 900 });
await expect(dialog).toHaveCount(1);
await expect(dialog).toBeVisible();
await expect(deckPage.newDeckName).toHaveValue("画面を回転しても残る調査");
await expect(deckPage.newDeckTitleInput).toHaveCount(1);
}
await deckPage.confirmCreate.click();
await expect(dialog).toHaveCount(0);
await expect(deckPage.heading).toHaveText("画面を回転しても残る調査");
});