hasolidit-mcp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "hasolidit-mcp",
3
+ "version": "0.0.1",
4
+ "author": "Daniel Golub",
5
+ "license": "CC-BY-NC-4.0",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node src/index.ts",
9
+ "lint": "eslint src/ tests/",
10
+ "lint:fix": "eslint src/ tests/ --fix",
11
+ "test": "npx playwright test",
12
+ "fixtures:update": "node scripts/capture-fixtures.ts"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.12.1",
16
+ "playwright": "^1.52.0"
17
+ },
18
+ "devDependencies": {
19
+ "@eslint/js": "^10.0.1",
20
+ "@playwright/test": "^1.52.0",
21
+ "@types/node": "^24.0.0",
22
+ "eslint": "^10.5.0",
23
+ "typescript-eslint": "^8.61.0"
24
+ }
25
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "@playwright/test";
2
+
3
+ export default defineConfig({
4
+ testDir: "./tests",
5
+ timeout: 60000,
6
+ testIgnore: process.env.CI ? /search\.test/ : undefined,
7
+ use: {
8
+ headless: true,
9
+ userAgent:
10
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
11
+ locale: "he-IL",
12
+ },
13
+ });
@@ -0,0 +1,44 @@
1
+ import { chromium } from "playwright";
2
+ import { writeFileSync, mkdirSync } from "fs";
3
+
4
+ mkdirSync("fixtures", { recursive: true });
5
+
6
+ const browser = await chromium.launch();
7
+ const ctx = await browser.newContext({
8
+ userAgent:
9
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
10
+ locale: "he-IL",
11
+ });
12
+
13
+ // Search results
14
+ console.log("Capturing search results...");
15
+ let page = await ctx.newPage();
16
+ await page.goto("https://www.hasolidit.com/kehila/search/?type=post");
17
+ await page
18
+ .locator('input[type="search"][name="keywords"]')
19
+ .fill("קרן השתלמות");
20
+ await page.getByRole("button", { name: "חיפוש" }).click();
21
+ await page.waitForSelector(".contentRow", { timeout: 15000 });
22
+ writeFileSync("fixtures/search-results.html", await page.content());
23
+ console.log(" Saved fixtures/search-results.html");
24
+
25
+ // Thread
26
+ console.log("Capturing thread...");
27
+ page = await ctx.newPage();
28
+ await page.goto(
29
+ "https://www.hasolidit.com/kehila/threads/%D7%94%D7%90%D7%9D-%D7%A2%D7%9C%D7%99-%D7%9C%D7%91%D7%A8%D7%95%D7%97-%D7%9E%D7%99%D7%A9%D7%A8%D7%90%D7%9C.34810/"
30
+ );
31
+ await page.waitForSelector(".message--post", { timeout: 15000 });
32
+ writeFileSync("fixtures/thread.html", await page.content());
33
+ console.log(" Saved fixtures/thread.html");
34
+
35
+ // New posts
36
+ console.log("Capturing new posts...");
37
+ page = await ctx.newPage();
38
+ await page.goto("https://www.hasolidit.com/kehila/whats-new/posts/");
39
+ await page.waitForSelector(".structItem--thread", { timeout: 15000 });
40
+ writeFileSync("fixtures/new-posts.html", await page.content());
41
+ console.log(" Saved fixtures/new-posts.html");
42
+
43
+ await browser.close();
44
+ console.log("Done.");
package/src/browser.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { chromium, type Browser, type BrowserContext } from "playwright";
2
+
3
+ let browser: Browser | null = null;
4
+
5
+ export async function getBrowser(): Promise<Browser> {
6
+ if (!browser) {
7
+ browser = await chromium.launch({ headless: true });
8
+ }
9
+ return browser;
10
+ }
11
+
12
+ export async function newContext(): Promise<BrowserContext> {
13
+ const b = await getBrowser();
14
+ return b.newContext({
15
+ userAgent:
16
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
17
+ locale: "he-IL",
18
+ });
19
+ }
20
+
21
+ export async function closeBrowser(): Promise<void> {
22
+ if (browser) {
23
+ await browser.close();
24
+ browser = null;
25
+ }
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { searchForum } from "./tools/search.ts";
5
+ import { viewThread } from "./tools/view-thread.ts";
6
+ import { getNewPosts } from "./tools/new-posts.ts";
7
+ import { closeBrowser } from "./browser.ts";
8
+ import { createRequire } from "node:module";
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const { version } = require("../package.json");
12
+
13
+ const server = new McpServer({
14
+ name: "hasolidit",
15
+ version,
16
+ });
17
+
18
+ server.tool(
19
+ "search_forum",
20
+ "Search the Hasolidit financial community forum. Returns titles, snippets, authors, dates, and links.",
21
+ {
22
+ query: z.string().describe("Search query (Hebrew or English)"),
23
+ page: z
24
+ .number()
25
+ .int()
26
+ .positive()
27
+ .optional()
28
+ .default(1)
29
+ .describe("Results page number (default 1)"),
30
+ },
31
+ async ({ query, page }) => {
32
+ const result = await searchForum(query, page);
33
+ return {
34
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
35
+ };
36
+ }
37
+ );
38
+
39
+ server.tool(
40
+ "view_thread",
41
+ "View a thread from the Hasolidit forum. Returns all posts with author, date, and content.",
42
+ {
43
+ url: z
44
+ .string()
45
+ .url()
46
+ .describe("Full URL of the thread to view"),
47
+ page: z
48
+ .number()
49
+ .int()
50
+ .positive()
51
+ .optional()
52
+ .default(1)
53
+ .describe("Thread page number (default 1)"),
54
+ },
55
+ async ({ url, page }) => {
56
+ const result = await viewThread(url, page);
57
+ return {
58
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
59
+ };
60
+ }
61
+ );
62
+
63
+ server.tool(
64
+ "new_posts",
65
+ "Get the latest active threads from the Hasolidit forum. Returns recently active threads with title, author, forum, reply/view counts, and last activity info.",
66
+ {},
67
+ async () => {
68
+ const result = await getNewPosts();
69
+ return {
70
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
71
+ };
72
+ }
73
+ );
74
+
75
+ async function main() {
76
+ const transport = new StdioServerTransport();
77
+ await server.connect(transport);
78
+
79
+ process.on("SIGINT", async () => {
80
+ await closeBrowser();
81
+ process.exit(0);
82
+ });
83
+ process.on("SIGTERM", async () => {
84
+ await closeBrowser();
85
+ process.exit(0);
86
+ });
87
+ }
88
+
89
+ main();
@@ -0,0 +1,82 @@
1
+ import { newContext } from "../browser.ts";
2
+
3
+ const URL = "https://www.hasolidit.com/kehila/whats-new/posts/";
4
+
5
+ export interface NewPostThread {
6
+ title: string;
7
+ url: string;
8
+ author: string;
9
+ forum: string;
10
+ replies: string;
11
+ views: string;
12
+ lastActivityDate: string;
13
+ lastPoster: string;
14
+ }
15
+
16
+ export interface NewPostsResponse {
17
+ threads: NewPostThread[];
18
+ }
19
+
20
+ export async function getNewPosts(): Promise<NewPostsResponse> {
21
+ const context = await newContext();
22
+ try {
23
+ const p = await context.newPage();
24
+
25
+ await p.goto(URL);
26
+ await p.waitForSelector(".structItem--thread", { timeout: 15000 });
27
+
28
+ const threads = await p.evaluate(() => {
29
+ return Array.from(
30
+ document.querySelectorAll(".structItem--thread")
31
+ ).map((item) => {
32
+ const titleEl = item.querySelector(".structItem-title a");
33
+ const title = titleEl?.textContent?.trim() ?? "";
34
+ const href = titleEl?.getAttribute("href") ?? "";
35
+ const url = href.startsWith("http")
36
+ ? href
37
+ : `https://www.hasolidit.com${href}`;
38
+
39
+ const parts = item.querySelectorAll(".structItem-parts li");
40
+ const author =
41
+ parts[0]?.querySelector(".username")?.textContent?.trim() ?? "";
42
+
43
+ const forumEl = item.querySelector(
44
+ '.structItem-parts a[href*="/forums/"]'
45
+ );
46
+ const forum = forumEl?.textContent?.trim() ?? "";
47
+
48
+ const metaDds = item.querySelectorAll(
49
+ ".structItem-cell--meta .pairs dd"
50
+ );
51
+ const replies = metaDds[0]?.textContent?.trim() ?? "0";
52
+ const views = metaDds[1]?.textContent?.trim() ?? "0";
53
+
54
+ const latestTime = item.querySelector(
55
+ ".structItem-cell--latest time"
56
+ );
57
+ const lastActivityDate =
58
+ latestTime?.getAttribute("datetime") ?? "";
59
+
60
+ const lastPoster =
61
+ item
62
+ .querySelector(".structItem-cell--latest .username")
63
+ ?.textContent?.trim() ?? "";
64
+
65
+ return {
66
+ title,
67
+ url,
68
+ author,
69
+ forum,
70
+ replies,
71
+ views,
72
+ lastActivityDate,
73
+ lastPoster,
74
+ };
75
+ });
76
+ });
77
+
78
+ return { threads };
79
+ } finally {
80
+ await context.close();
81
+ }
82
+ }
@@ -0,0 +1,111 @@
1
+ import { newContext } from "../browser.ts";
2
+
3
+ const BASE_URL = "https://www.hasolidit.com/kehila";
4
+
5
+ export interface SearchResult {
6
+ title: string;
7
+ url: string;
8
+ snippet: string;
9
+ author: string;
10
+ date: string;
11
+ forum: string;
12
+ replyCount: string | null;
13
+ }
14
+
15
+ export interface SearchResponse {
16
+ query: string;
17
+ page: number;
18
+ totalPages: number;
19
+ results: SearchResult[];
20
+ }
21
+
22
+ export async function searchForum(
23
+ query: string,
24
+ page: number = 1
25
+ ): Promise<SearchResponse> {
26
+ const context = await newContext();
27
+ try {
28
+ const p = await context.newPage();
29
+
30
+ // Navigate to search page
31
+ await p.goto(`${BASE_URL}/search/?type=post`);
32
+
33
+ // Fill and submit search form
34
+ await p.locator('input[type="search"][name="keywords"]').fill(query);
35
+ await p.getByRole("button", { name: "חיפוש" }).click();
36
+
37
+ // Wait for results page
38
+ await p.waitForSelector(".contentRow", { timeout: 15000 });
39
+
40
+ // If requesting a page beyond 1, navigate to it
41
+ if (page > 1) {
42
+ const currentUrl = p.url();
43
+ const url = new URL(currentUrl);
44
+ url.searchParams.set("page", String(page));
45
+ await p.goto(url.toString());
46
+ await p.waitForSelector(".contentRow", { timeout: 10000 });
47
+ }
48
+
49
+ // Extract pagination
50
+ const totalPages = await extractTotalPages(p);
51
+
52
+ // Extract results
53
+ const rows = p.locator(".contentRow");
54
+ const count = await rows.count();
55
+ const results: SearchResult[] = [];
56
+
57
+ for (let i = 0; i < count; i++) {
58
+ const row = rows.nth(i);
59
+
60
+ const titleEl = row.locator(".contentRow-title a");
61
+ const title = (await titleEl.textContent()) ?? "";
62
+ const href = (await titleEl.getAttribute("href")) ?? "";
63
+ const url = href.startsWith("http")
64
+ ? href
65
+ : `https://www.hasolidit.com${href}`;
66
+
67
+ const snippet =
68
+ (await row.locator(".contentRow-snippet").textContent()) ?? "";
69
+
70
+ const minor = row.locator(".contentRow-minor .listInline li");
71
+ const minorCount = await minor.count();
72
+ const minorTexts: string[] = [];
73
+ for (let j = 0; j < minorCount; j++) {
74
+ minorTexts.push(
75
+ ((await minor.nth(j).textContent()) ?? "").trim()
76
+ );
77
+ }
78
+
79
+ // First item is author, date is usually 3rd, forum is last with "פורום:" prefix
80
+ const author = minorTexts[0] ?? "";
81
+ const date = minorTexts[2] ?? "";
82
+ const forumEntry = minorTexts.find((t) => t.startsWith("פורום:"));
83
+ const forum = forumEntry?.replace("פורום: ", "") ?? "";
84
+ const replyEntry = minorTexts.find((t) => t.startsWith("תגובות:"));
85
+ const replyCount = replyEntry?.replace("תגובות: ", "") ?? null;
86
+
87
+ results.push({
88
+ title: title.trim(),
89
+ url,
90
+ snippet: snippet.trim(),
91
+ author,
92
+ date,
93
+ forum,
94
+ replyCount,
95
+ });
96
+ }
97
+
98
+ return { query, page, totalPages, results };
99
+ } finally {
100
+ await context.close();
101
+ }
102
+ }
103
+
104
+ async function extractTotalPages(
105
+ p: import("playwright").Page
106
+ ): Promise<number> {
107
+ const pageNav = p.locator(".pageNav-page").last();
108
+ if ((await pageNav.count()) === 0) return 1;
109
+ const lastPageText = (await pageNav.textContent()) ?? "1";
110
+ return parseInt(lastPageText.trim(), 10) || 1;
111
+ }
@@ -0,0 +1,85 @@
1
+ import { newContext } from "../browser.ts";
2
+
3
+ export interface Post {
4
+ author: string;
5
+ date: string;
6
+ content: string;
7
+ postUrl: string;
8
+ }
9
+
10
+ export interface ThreadResponse {
11
+ title: string;
12
+ url: string;
13
+ page: number;
14
+ totalPages: number;
15
+ posts: Post[];
16
+ }
17
+
18
+ export async function viewThread(
19
+ url: string,
20
+ page: number = 1
21
+ ): Promise<ThreadResponse> {
22
+ const context = await newContext();
23
+ try {
24
+ const p = await context.newPage();
25
+
26
+ // XenForo pagination: append page-N to the URL path
27
+ let targetUrl = url;
28
+ if (page > 1) {
29
+ targetUrl = url.replace(/\/$/, "") + `/page-${page}`;
30
+ }
31
+
32
+ await p.goto(targetUrl);
33
+ await p.waitForSelector(".message--post", { timeout: 15000 });
34
+
35
+ // Extract everything in a single evaluate call — avoids per-element timeouts
36
+ const data = await p.evaluate(() => {
37
+ const title =
38
+ document.querySelector("h1.p-title-value")?.textContent?.trim() ?? "";
39
+
40
+ const lastPageEl = document.querySelector(
41
+ ".pageNav-page:last-child a"
42
+ );
43
+ const totalPages = lastPageEl
44
+ ? parseInt(lastPageEl.textContent?.trim() ?? "1", 10) || 1
45
+ : 1;
46
+
47
+ const posts = Array.from(
48
+ document.querySelectorAll("article.message--post")
49
+ ).map((article) => {
50
+ const author =
51
+ article.querySelector("h4.message-name")?.textContent?.trim() ?? "";
52
+
53
+ const timeEl = article.querySelector(
54
+ ".message-attribution-main time"
55
+ );
56
+ const date = timeEl?.getAttribute("datetime") ?? "";
57
+
58
+ const bbWrapper = article.querySelector(".message-body .bbWrapper");
59
+ const content = bbWrapper?.textContent?.trim() ?? "";
60
+
61
+ const postLink = article.querySelector(
62
+ "a.message-attribution-gadget"
63
+ );
64
+ const postHref = postLink?.getAttribute("href") ?? "";
65
+
66
+ return { author, date, content, postHref };
67
+ });
68
+
69
+ return { title, totalPages, posts };
70
+ });
71
+
72
+ const posts: Post[] = data.posts.map((p) => ({
73
+ author: p.author,
74
+ date: p.date,
75
+ content: p.content,
76
+ postUrl: p.postHref.startsWith("http")
77
+ ? p.postHref
78
+ : `https://www.hasolidit.com${p.postHref}`,
79
+ }));
80
+
81
+ return { title: data.title, url: targetUrl, page, totalPages: data.totalPages, posts };
82
+ } finally {
83
+ await context.close();
84
+ }
85
+ }
@@ -0,0 +1,32 @@
1
+ # Tests
2
+
3
+ E2E tests using Playwright against the Hasolidit forum.
4
+
5
+ ## Running tests
6
+
7
+ - `npm test` — runs all tests locally against the live site, or fixtures in CI
8
+
9
+ ## Local vs CI
10
+
11
+ - **Locally**: all 6 tests run against the live site (real browser, real network)
12
+ - **CI**: tests use saved HTML fixtures from `fixtures/`. Search tests are skipped in CI because they require form interaction that can't be replayed from a static fixture.
13
+
14
+ ## How it works
15
+
16
+ Tests import `test` and `expect` from `./test.ts` (not `@playwright/test` directly). This custom fixture automatically intercepts network requests in CI and serves saved HTML from `fixtures/`. Locally it passes through to the real site unchanged.
17
+
18
+ ## Adding tests
19
+
20
+ 1. Import from `./test.ts`: `import { test, expect } from "./test.ts";`
21
+ 2. Write tests using standard Playwright API — they work against the live site
22
+ 3. If the test hits a new URL pattern, add a route mapping in `tests/test.ts` and capture a fixture for it
23
+
24
+ ## Timeouts
25
+
26
+ - Test timeout: 60s (set in `playwright.config.ts`)
27
+ - `waitForSelector` timeout: 30s per call
28
+ - CI retries: 1
29
+
30
+ ## Note
31
+
32
+ These tests validate that our DOM selectors match the real site structure. If the site changes its HTML, tests will fail locally — update selectors and run `npm run fixtures:update` to recapture.
@@ -0,0 +1,60 @@
1
+ import { test, expect } from "./test.ts";
2
+
3
+ const URL = "https://www.hasolidit.com/kehila/whats-new/posts/";
4
+
5
+ test.describe("new_posts", () => {
6
+ test("loads recent threads with expected structure", async ({ page }) => {
7
+ await page.goto(URL);
8
+ await page.waitForSelector(".structItem--thread", { timeout: 30000 });
9
+
10
+ const items = page.locator(".structItem--thread");
11
+ const count = await items.count();
12
+ expect(count).toBeGreaterThan(0);
13
+
14
+ // First thread has a title with a link
15
+ const first = items.first();
16
+ const titleLink = first.locator(".structItem-title a").first();
17
+ const title = await titleLink.textContent();
18
+ expect(title?.trim().length).toBeGreaterThan(0);
19
+
20
+ const href = await titleLink.getAttribute("href");
21
+ expect(href).toContain("/threads/");
22
+
23
+ // Has an author
24
+ const author = await first
25
+ .locator(".structItem-parts .username")
26
+ .first()
27
+ .textContent();
28
+ expect(author?.trim().length).toBeGreaterThan(0);
29
+
30
+ // Has reply count
31
+ const replies = await first
32
+ .locator(".structItem-cell--meta .pairs dd")
33
+ .first()
34
+ .textContent();
35
+ expect(replies?.trim()).toBeTruthy();
36
+
37
+ // Has last activity date
38
+ const latestTime = first.locator(".structItem-cell--latest time");
39
+ const datetime = await latestTime.getAttribute("datetime");
40
+ expect(datetime).toBeTruthy();
41
+
42
+ // Has last poster
43
+ const lastPoster = await first
44
+ .locator(".structItem-cell--latest .username")
45
+ .textContent();
46
+ expect(lastPoster?.trim().length).toBeGreaterThan(0);
47
+ });
48
+
49
+ test("has forum info for threads", async ({ page }) => {
50
+ await page.goto(URL);
51
+ await page.waitForSelector(".structItem--thread", { timeout: 30000 });
52
+
53
+ const first = page.locator(".structItem--thread").first();
54
+ const forumLink = first.locator(
55
+ '.structItem-parts a[href*="/forums/"]'
56
+ );
57
+ const forum = await forumLink.textContent();
58
+ expect(forum?.trim().length).toBeGreaterThan(0);
59
+ });
60
+ });
@@ -0,0 +1,65 @@
1
+ import { test, expect } from "./test.ts";
2
+
3
+ const BASE_URL = "https://www.hasolidit.com/kehila";
4
+
5
+ test.describe("search_forum", () => {
6
+ test("returns results for a known query", async ({ page }) => {
7
+ // Navigate to search page
8
+ await page.goto(`${BASE_URL}/search/?type=post`);
9
+
10
+ // Fill and submit
11
+ await page.locator('input[type="search"][name="keywords"]').fill("קרן השתלמות");
12
+ await page.getByRole("button", { name: "חיפוש" }).click();
13
+
14
+ // Wait for results
15
+ await page.waitForSelector(".contentRow", { timeout: 30000 });
16
+
17
+ // Verify results exist
18
+ const rows = page.locator(".contentRow");
19
+ const count = await rows.count();
20
+ expect(count).toBeGreaterThan(0);
21
+
22
+ // Verify first result has expected structure
23
+ const first = rows.first();
24
+ const title = await first.locator(".contentRow-title a").textContent();
25
+ expect(title?.trim().length).toBeGreaterThan(0);
26
+
27
+ const href = await first
28
+ .locator(".contentRow-title a")
29
+ .getAttribute("href");
30
+ expect(href).toBeTruthy();
31
+
32
+ const snippet = await first
33
+ .locator(".contentRow-snippet")
34
+ .textContent();
35
+ expect(snippet?.trim().length).toBeGreaterThan(0);
36
+
37
+ // Verify metadata
38
+ const minorItems = first.locator(".contentRow-minor .listInline li");
39
+ const minorCount = await minorItems.count();
40
+ expect(minorCount).toBeGreaterThanOrEqual(3); // author, type, date at minimum
41
+ });
42
+
43
+ test("pagination works", async ({ page }) => {
44
+ await page.goto(`${BASE_URL}/search/?type=post`);
45
+ await page.locator('input[type="search"][name="keywords"]').fill("מס הכנסה");
46
+ await page.getByRole("button", { name: "חיפוש" }).click();
47
+ await page.waitForSelector(".contentRow", { timeout: 30000 });
48
+
49
+ // Check pagination exists
50
+ const pageNav = page.locator(".pageNav-page");
51
+ const pageCount = await pageNav.count();
52
+ expect(pageCount).toBeGreaterThan(1);
53
+
54
+ // Navigate to page 2
55
+ const currentUrl = page.url();
56
+ const url = new URL(currentUrl);
57
+ url.searchParams.set("page", "2");
58
+ await page.goto(url.toString());
59
+ await page.waitForSelector(".contentRow", { timeout: 30000 });
60
+
61
+ // Verify page 2 has results
62
+ const rows = page.locator(".contentRow");
63
+ expect(await rows.count()).toBeGreaterThan(0);
64
+ });
65
+ });
package/tests/test.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { test as base } from "@playwright/test";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ export { expect } from "@playwright/test";
6
+
7
+ const fixtureDir = join(import.meta.dirname, "..", "fixtures");
8
+
9
+ const routes: [string, string][] = [
10
+ ["**/kehila/search/**", "search-results.html"],
11
+ ["**/kehila/threads/**", "thread.html"],
12
+ ["**/kehila/whats-new/posts/**", "new-posts.html"],
13
+ ];
14
+
15
+ export const test = process.env.CI
16
+ ? base.extend({
17
+ page: async ({ page }, use) => {
18
+ for (const [pattern, file] of routes) {
19
+ const html = readFileSync(join(fixtureDir, file), "utf-8");
20
+ await page.route(pattern, (route) =>
21
+ route.fulfill({
22
+ status: 200,
23
+ contentType: "text/html",
24
+ body: html,
25
+ })
26
+ );
27
+ }
28
+ await use(page);
29
+ },
30
+ })
31
+ : base;