Crawler Scraper Skill
Checklist (MUST complete all)
File Locations
| Purpose |
Location |
| URLs |
packages/meta/src/urls.ts |
| Scrapers |
apps/crawler/src/scrapers/*.ts |
| Types |
packages/db/src/types.ts |
| Repositories |
packages/db/src/repositories/ |
| Parsers |
apps/crawler/src/parsers.ts |
| Entry point |
apps/crawler/src/index.ts |
Template
// apps/crawler/src/scrapers/my-feature.ts
import type { MyData } from "@mf-dashboard/db/types";
import type { Page } from "playwright";
import { mfUrls } from "@mf-dashboard/meta";
import { debug } from "../logger.js";
import { parseJapaneseNumber } from "../parsers.js";
export async function getMyData(page: Page): Promise<MyData> {
debug("Getting my data from /path...");
await page.goto(mfUrls.myFeature, {
waitUntil: "domcontentloaded",
});
await page.waitForTimeout(2000);
// Scraping logic here...
const rows = page.locator("table tbody tr");
const count = await rows.count();
const results: MyItem[] = [];
for (let i = 0; i < count; i++) {
const row = rows.nth(i);
const text = await row
.locator("td")
.first()
.textContent({ timeout: 1000 })
.catch(() => "");
results.push({
// parsed data
});
}
return { items: results };
}
URL Registration
// packages/meta/src/urls.ts
export const mfUrls = {
// existing urls...
myFeature: "https://moneyforward.com/path/to/feature",
};
Testing
Test Types and Priority
| Priority |
Type |
When to Use |
Location |
| 1 |
Unit |
Post-extraction parsing, normalization, comparison, mapping, and fail-closed decisions using anonymous strings or objects |
*.test.ts next to source |
| 2 |
E2E |
Selectors, navigation, and required HTML/DOM structure on the authenticated real service |
tests/e2e/*.test.ts in the e2e Vitest project |
| 3 |
HTML fixture exception |
A failure branch that cannot be produced safely and deterministically against the real service |
The narrowest applicable unit test |
Rules (MUST follow)
- Extract DOM values into strings or objects, then test the resulting pure transformation and decision logic without Playwright or embedded HTML.
- Verify selector compatibility and page structure with authenticated read-only E2E tests. Do not trigger refresh, account updates, crawling, or database writes in structure-only E2E tests.
- NEVER write assertions that depend on actual financial data. Do not assert or log real names, balances, account IDs, or other personal values.
- E2E assertions may check navigation and structural properties only, including the presence and shape of headings, tables, rows, cells, attributes, and links.
- Bound structure-only E2E navigation independently from production crawl coverage. If production scans every account for correctness, inspect at most one representative detail page in E2E, skip when no suitable candidate exists, and document the scope difference in the test and pull request.
- Do not use embedded HTML fixtures merely to imitate the current service DOM. They are allowed only when a failure branch cannot be represented safely and deterministically in read-only E2E.
- For every HTML fixture exception, keep the markup to the minimum needed and add a nearby comment explaining why authenticated read-only E2E cannot cover that branch.
- Use anonymous hardcoded strings and objects for unit tests; never copy values from the production database or authenticated pages.
Running Tests
- Unit:
pnpm --filter @mf-dashboard/crawler test
- E2E:
pnpm --filter @mf-dashboard/crawler test:e2e
- Local manual testing:
SKIP_REFRESH=true pnpm --filter @mf-dashboard/crawler start
- Debug scripts go in
debug/ directory
- Screenshots saved to
debug/ directory
Notes
- Use
parseJapaneseNumber() for Japanese currency format (e.g., "1,234円" → 1234)
- Use
debug() from logger for debug output
- Handle missing elements gracefully with
.catch(() => defaultValue)
- Always use
{ timeout: 1000 } for individual element queries to avoid hanging
1---2name: crawler-scraper3description: Use when adding new scraping targets to the crawler4---56# Crawler Scraper Skill78## Checklist (MUST complete all)910- [ ] Add URL to `packages/meta/src/urls.ts`11- [ ] Create scraper function in `apps/crawler/src/scrapers/`12- [ ] Define types in `packages/db/src/types.ts`13- [ ] Add repository if new data storage needed14- [ ] Integrate into `apps/crawler/src/index.ts`15- [ ] Add DOM-independent unit tests for parser decisions and transformations16- [ ] Add authenticated read-only E2E coverage when selectors, navigation, or page structure change1718## File Locations1920| Purpose | Location |21| ------------ | -------------------------------- |22| URLs | `packages/meta/src/urls.ts` |23| Scrapers | `apps/crawler/src/scrapers/*.ts` |24| Types | `packages/db/src/types.ts` |25| Repositories | `packages/db/src/repositories/` |26| Parsers | `apps/crawler/src/parsers.ts` |27| Entry point | `apps/crawler/src/index.ts` |2829## Template3031```typescript32// apps/crawler/src/scrapers/my-feature.ts33import type { MyData } from "@mf-dashboard/db/types";34import type { Page } from "playwright";35import { mfUrls } from "@mf-dashboard/meta";36import { debug } from "../logger.js";37import { parseJapaneseNumber } from "../parsers.js";3839export async function getMyData(page: Page): Promise<MyData> {40 debug("Getting my data from /path...");4142 await page.goto(mfUrls.myFeature, {43 waitUntil: "domcontentloaded",44 });45 await page.waitForTimeout(2000);4647 // Scraping logic here...48 const rows = page.locator("table tbody tr");49 const count = await rows.count();5051 const results: MyItem[] = [];52 for (let i = 0; i < count; i++) {53 const row = rows.nth(i);54 const text = await row55 .locator("td")56 .first()57 .textContent({ timeout: 1000 })58 .catch(() => "");5960 results.push({61 // parsed data62 });63 }6465 return { items: results };66}67```6869## URL Registration7071```typescript72// packages/meta/src/urls.ts73export const mfUrls = {74 // existing urls...75 myFeature: "https://moneyforward.com/path/to/feature",76};77```7879## Testing8081### Test Types and Priority8283| Priority | Type | When to Use | Location |84| -------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |85| 1 | Unit | Post-extraction parsing, normalization, comparison, mapping, and fail-closed decisions using anonymous strings or objects | `*.test.ts` next to source |86| 2 | E2E | Selectors, navigation, and required HTML/DOM structure on the authenticated real service | `tests/e2e/*.test.ts` in the `e2e` Vitest project |87| 3 | HTML fixture exception | A failure branch that cannot be produced safely and deterministically against the real service | The narrowest applicable unit test |8889### Rules (MUST follow)9091- Extract DOM values into strings or objects, then test the resulting pure transformation and decision logic without Playwright or embedded HTML.92- Verify selector compatibility and page structure with authenticated **read-only** E2E tests. Do not trigger refresh, account updates, crawling, or database writes in structure-only E2E tests.93- **NEVER write assertions that depend on actual financial data.** Do not assert or log real names, balances, account IDs, or other personal values.94- E2E assertions may check navigation and structural properties only, including the presence and shape of headings, tables, rows, cells, attributes, and links.95- Bound structure-only E2E navigation independently from production crawl coverage. If production scans every account for correctness, inspect at most one representative detail page in E2E, skip when no suitable candidate exists, and document the scope difference in the test and pull request.96- Do not use embedded HTML fixtures merely to imitate the current service DOM. They are allowed only when a failure branch cannot be represented safely and deterministically in read-only E2E.97- For every HTML fixture exception, keep the markup to the minimum needed and add a nearby comment explaining why authenticated read-only E2E cannot cover that branch.98- Use anonymous hardcoded strings and objects for unit tests; never copy values from the production database or authenticated pages.99100### Running Tests101102- Unit: `pnpm --filter @mf-dashboard/crawler test`103- E2E: `pnpm --filter @mf-dashboard/crawler test:e2e`104- Local manual testing: `SKIP_REFRESH=true pnpm --filter @mf-dashboard/crawler start`105- Debug scripts go in `debug/` directory106- Screenshots saved to `debug/` directory107108## Notes109110- Use `parseJapaneseNumber()` for Japanese currency format (e.g., "1,234円" → 1234)111- Use `debug()` from logger for debug output112- Handle missing elements gracefully with `.catch(() => defaultValue)`113- Always use `{ timeout: 1000 }` for individual element queries to avoid hanging