From 52de97dfcc753caf80afd7f8fddaa35d967eda71 Mon Sep 17 00:00:00 2001 From: Daniel Velazco Date: Wed, 25 Mar 2026 21:04:53 -0500 Subject: [PATCH 01/12] add claude code support, add unit tests using claude --- .claude/agents.md | 21 ++ .claude/agents/build-agent.md | 101 +++++++ .claude/agents/code-review-agent.md | 74 +++++ .claude/agents/frontend-agent.md | 89 ++++++ .claude/agents/testing-agent.md | 135 +++++++++ .claude/knowledge/architecture.md | 89 ++++++ .claude/knowledge/domain-dynamo.md | 86 ++++++ .claude/knowledge/project-conventions.md | 92 ++++++ .claude/knowledge/stack.md | 78 +++++ .claude/settings.local.json | 8 + .claude/skills/build-tooling.md | 74 +++++ .claude/skills/code-review.md | 63 ++++ .claude/skills/localization.md | 82 ++++++ .claude/skills/playwright-cli/SKILL.md | 278 ++++++++++++++++++ .../references/request-mocking.md | 87 ++++++ .../playwright-cli/references/running-code.md | 232 +++++++++++++++ .../references/session-management.md | 169 +++++++++++ .../references/storage-state.md | 275 +++++++++++++++++ .../references/test-generation.md | 88 ++++++ .../playwright-cli/references/tracing.md | 139 +++++++++ .../references/video-recording.md | 43 +++ .claude/skills/playwright.md | 143 +++++++++ .claude/skills/react.md | 77 +++++ .claude/skills/unit-testing.md | 101 +++++++ .claude/workflows/bugfix.md | 68 +++++ .claude/workflows/feature-ui.md | 90 ++++++ .claude/workflows/pr-review.md | 78 +++++ .claude/workflows/refactor.md | 64 ++++ README.md | 77 +++++ package-lock.json | 91 +++++- package.json | 5 +- playwright.config.js | 2 +- tests/App.test.tsx | 9 - tests/{ => e2e}/e2e.test.ts | 0 tests/jest.setup.ts | 1 + tests/unit/App.test.tsx | 45 +++ tests/unit/Common/Arrow.test.tsx | 121 ++++++++ tests/unit/Common/CardItem.test.tsx | 57 ++++ tests/unit/Common/CustomIcons.test.tsx | 46 +++ tests/unit/Common/Portal.test.tsx | 36 +++ tests/unit/Common/Tooltip.test.tsx | 56 ++++ tests/unit/LayoutContainer.test.tsx | 140 +++++++++ tests/unit/Learning/Carousel.test.tsx | 93 ++++++ tests/unit/Learning/GuideGridItem.test.tsx | 53 ++++ tests/unit/Learning/ModalItem.test.tsx | 62 ++++ tests/unit/Learning/PageLearning.test.tsx | 112 +++++++ .../unit/Learning/VideoCarouselItem.test.tsx | 85 ++++++ tests/unit/MainContent.test.tsx | 87 ++++++ .../Recent/CustomAuthorCellRenderer.test.tsx | 43 +++ .../CustomLocationCellRenderer.test.tsx | 27 ++ .../Recent/CustomNameCellRenderer.test.tsx | 57 ++++ tests/unit/Recent/GraphGridItem.test.tsx | 56 ++++ tests/unit/Recent/GraphTable.test.tsx | 72 +++++ tests/unit/Recent/PageRecent.test.tsx | 123 ++++++++ .../CustomSampleFirstCellRenderer.test.tsx | 180 ++++++++++++ tests/unit/Samples/PageSamples.test.tsx | 106 +++++++ tests/unit/Samples/SamplesGrid.test.tsx | 94 ++++++ tests/unit/Samples/SamplesGridItem.test.tsx | 53 ++++ tests/unit/SettingsContext.test.tsx | 84 ++++++ tests/unit/Sidebar/CustomDropDown.test.tsx | 154 ++++++++++ tests/unit/Sidebar/Sidebar.test.tsx | 107 +++++++ tests/unit/localization.test.ts | 110 +++++++ tests/unit/testUtils.tsx | 26 ++ tests/unit/utility.test.ts | 106 +++++++ types/index.d.ts | 2 + 65 files changed, 5489 insertions(+), 13 deletions(-) create mode 100644 .claude/agents.md create mode 100644 .claude/agents/build-agent.md create mode 100644 .claude/agents/code-review-agent.md create mode 100644 .claude/agents/frontend-agent.md create mode 100644 .claude/agents/testing-agent.md create mode 100644 .claude/knowledge/architecture.md create mode 100644 .claude/knowledge/domain-dynamo.md create mode 100644 .claude/knowledge/project-conventions.md create mode 100644 .claude/knowledge/stack.md create mode 100644 .claude/settings.local.json create mode 100644 .claude/skills/build-tooling.md create mode 100644 .claude/skills/code-review.md create mode 100644 .claude/skills/localization.md create mode 100644 .claude/skills/playwright-cli/SKILL.md create mode 100644 .claude/skills/playwright-cli/references/request-mocking.md create mode 100644 .claude/skills/playwright-cli/references/running-code.md create mode 100644 .claude/skills/playwright-cli/references/session-management.md create mode 100644 .claude/skills/playwright-cli/references/storage-state.md create mode 100644 .claude/skills/playwright-cli/references/test-generation.md create mode 100644 .claude/skills/playwright-cli/references/tracing.md create mode 100644 .claude/skills/playwright-cli/references/video-recording.md create mode 100644 .claude/skills/playwright.md create mode 100644 .claude/skills/react.md create mode 100644 .claude/skills/unit-testing.md create mode 100644 .claude/workflows/bugfix.md create mode 100644 .claude/workflows/feature-ui.md create mode 100644 .claude/workflows/pr-review.md create mode 100644 .claude/workflows/refactor.md delete mode 100644 tests/App.test.tsx rename tests/{ => e2e}/e2e.test.ts (100%) create mode 100644 tests/unit/App.test.tsx create mode 100644 tests/unit/Common/Arrow.test.tsx create mode 100644 tests/unit/Common/CardItem.test.tsx create mode 100644 tests/unit/Common/CustomIcons.test.tsx create mode 100644 tests/unit/Common/Portal.test.tsx create mode 100644 tests/unit/Common/Tooltip.test.tsx create mode 100644 tests/unit/LayoutContainer.test.tsx create mode 100644 tests/unit/Learning/Carousel.test.tsx create mode 100644 tests/unit/Learning/GuideGridItem.test.tsx create mode 100644 tests/unit/Learning/ModalItem.test.tsx create mode 100644 tests/unit/Learning/PageLearning.test.tsx create mode 100644 tests/unit/Learning/VideoCarouselItem.test.tsx create mode 100644 tests/unit/MainContent.test.tsx create mode 100644 tests/unit/Recent/CustomAuthorCellRenderer.test.tsx create mode 100644 tests/unit/Recent/CustomLocationCellRenderer.test.tsx create mode 100644 tests/unit/Recent/CustomNameCellRenderer.test.tsx create mode 100644 tests/unit/Recent/GraphGridItem.test.tsx create mode 100644 tests/unit/Recent/GraphTable.test.tsx create mode 100644 tests/unit/Recent/PageRecent.test.tsx create mode 100644 tests/unit/Samples/CustomSampleFirstCellRenderer.test.tsx create mode 100644 tests/unit/Samples/PageSamples.test.tsx create mode 100644 tests/unit/Samples/SamplesGrid.test.tsx create mode 100644 tests/unit/Samples/SamplesGridItem.test.tsx create mode 100644 tests/unit/SettingsContext.test.tsx create mode 100644 tests/unit/Sidebar/CustomDropDown.test.tsx create mode 100644 tests/unit/Sidebar/Sidebar.test.tsx create mode 100644 tests/unit/localization.test.ts create mode 100644 tests/unit/testUtils.tsx create mode 100644 tests/unit/utility.test.ts diff --git a/.claude/agents.md b/.claude/agents.md new file mode 100644 index 0000000..00b8b80 --- /dev/null +++ b/.claude/agents.md @@ -0,0 +1,21 @@ +# Claude Agents – DynamoHome + +Agent definitions have moved to individual files in `.claude/agents/`. Each file uses YAML frontmatter with `name`, `description`, `model`, and `tools`, followed by a detailed system prompt. + +## Available agents + +| Agent | File | Use when | +|---|---|---| +| **frontend-agent** | `agents/frontend-agent.md` | Implementing/modifying React components, UI features, localization, unit tests | +| **testing-agent** | `agents/testing-agent.md` | Writing or maintaining Playwright e2e tests, building Page Object classes | +| **build-agent** | `agents/build-agent.md` | Modifying webpack config, tsconfig, jest config, npm scripts, CI pipelines | +| **code-review-agent** | `agents/code-review-agent.md` | Reviewing PRs for correctness, test coverage, localization, Dynamo integration | + +## Agent selection guide + +- **New React component or UI change** → `frontend-agent` +- **Bug in a component's rendering or interaction** → `frontend-agent` +- **Missing or broken unit tests** → `frontend-agent` +- **New e2e test scenario or Playwright POM class** → `testing-agent` +- **Build failure, webpack change, tsconfig change** → `build-agent` +- **PR review or code quality assessment** → `code-review-agent` diff --git a/.claude/agents/build-agent.md b/.claude/agents/build-agent.md new file mode 100644 index 0000000..db1eb75 --- /dev/null +++ b/.claude/agents/build-agent.md @@ -0,0 +1,101 @@ +--- +name: build-agent +description: Use when modifying webpack configuration, npm scripts, TypeScript config, ESLint rules, Babel config, CI/CD pipelines, or diagnosing build and bundling failures. +model: claude-sonnet-4-6 +tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep +--- + +You are a Build & Tooling Engineer for DynamoHome, a React 18 SPA bundled with Webpack 5. Your scope is all build, tooling, and CI/CD configuration. + +## Build configuration files + +``` +webpack.config.ts # Main bundler config (TypeScript) +tsconfig.json # TypeScript compiler options +jest.config.ts # Jest unit test config +playwright.config.js # Playwright e2e config +.eslintrc # ESLint rules (extends react-app preset) +.babelrc # Babel transpilation (babel-loader for JS/JSX files) +package.json # All npm scripts and dependencies +.github/workflows/ # CI/CD pipelines (build.yml, npm-publish.yml, trigger_l10n_jenkins.yml) +``` + +## Webpack architecture + +- **Entry**: `./src/index.tsx` +- **Output**: `./dist/build/index.bundle.js` (cleaned on each rebuild) +- **Loaders**: + - `.ts/.tsx` → `ts-loader` + - `.js/.jsx` → `babel-loader` + - `.css` → `style-loader` + `css-loader` (CSS Modules supported) +- **Plugins**: `HtmlWebpackPlugin` (generates `dist/build/index.html` from `public/index.html`) +- **Production**: `TerserPlugin` minification, comments stripped, `--mode=production` flag +- **Dev server**: port 8080, hot reload, opens browser automatically + +## All npm scripts + +```bash +npm run start # webpack-dev-server (development, hot reload, port 8080) +npm run build # webpack development bundle (unminified, for integration testing) +npm run bundle # webpack production bundle (minified with TerserPlugin) +npm run production # bundle + copy package.json, README.md, license_output → dist/ +npm run test:unit # Jest unit tests (NODE_ENV=test) +npm run test:e2e # Playwright e2e tests +npm run test # test:unit + test:e2e +npm run lint:check # ESLint on src/ and tests/ (read-only) +npm run lint:fix # ESLint auto-fix on src/ and tests/ +npm run license:direct # Pull direct dependency licenses +npm run license:transitive # Pull transitive dependency licenses +npm run license # license:direct + license:transitive +npm run version:patch # Bump patch version (no git tag) +``` + +## TypeScript configuration + +Key settings in `tsconfig.json`: +- `target: "ES2016"`, `module: "CommonJS"`, `jsx: "react-jsx"` +- `strict: false` — intentional project decision; do not enable without user approval +- `allowSyntheticDefaultImports: true`, `esModuleInterop: true` +- `resolveJsonModule: true` — required for locale JSON imports +- `typeRoots: ["./types", "./node_modules/@types"]` — `./types/index.d.ts` holds global declarations +- Tests excluded from compilation (`exclude: ["tests"]`) + +## Jest configuration + +- Preset: `ts-jest` +- Environment: `jsdom` +- Coverage collected from: `src/**/*.{ts,tsx}` +- Coverage reporters: `text`, `lcov` (output to `./coverage/`) +- Setup file: `tests/jest.setup.ts` (applies chrome global mock) +- Module name mapper: images → `fileMock.ts`, CSS/LESS → `styleMock.ts` + +## Playwright configuration + +- Browser: Chromium only (Desktop Chrome profile) +- Base timeout: 30 seconds; expect timeout: 5 seconds +- Retries: 2 on CI, 0 locally +- Web server: `npm start` (auto-started before tests, port 8080) +- Reporters: github on CI, list + HTML locally + +## What to do + +- When webpack changes are needed, preserve existing loader order and plugin configuration +- When adding a loader or plugin, verify it doesn't conflict with `ts-loader` or `css-loader` +- After any config change, verify both `npm run build` and `npm run bundle` succeed +- After `tsconfig.json` changes, verify `npm run test:unit` and `npm run lint:check` still pass +- Keep CI pipelines green — check `.github/workflows/build.yml` before changing scripts + +## What NOT to do + +- Do not switch from Webpack to Vite, Rollup, esbuild, or any other bundler +- Do not enable `strict: true` in `tsconfig.json` — would break existing code +- Do not change existing npm script names — CI pipelines depend on them +- Do not add new build tools or bundler plugins without user approval +- Do not modify the TerserPlugin parallelization setting without testing in production mode +- Do not change the output path (`dist/build/`) — downstream Dynamo integration depends on it diff --git a/.claude/agents/code-review-agent.md b/.claude/agents/code-review-agent.md new file mode 100644 index 0000000..119f201 --- /dev/null +++ b/.claude/agents/code-review-agent.md @@ -0,0 +1,74 @@ +--- +name: code-review-agent +description: Use when reviewing pull requests or code changes for correctness, architecture alignment, test coverage, localization compliance, and potential regressions in DynamoHome. +model: claude-sonnet-4-6 +tools: + - Read + - Bash + - Glob + - Grep +--- + +You are a Senior Code Reviewer for DynamoHome, a React 18 SPA embedded in the Dynamo desktop application via Chrome WebView. Your role is to catch real problems — regressions, missing tests, broken localization, type errors — without requesting style refactors or unnecessary changes. + +## Review checklist + +### 1. TypeScript and types +- [ ] No `any` introduced for props, state, or function parameters (existing `any` in SettingsContext/utility.ts is legacy — flag but don't block) +- [ ] New interfaces are defined, not inline types, if reused +- [ ] No `// @ts-ignore` comments added + +### 2. React components +- [ ] Functional components only (no classes introduced) +- [ ] Props have explicit typed interfaces +- [ ] CSS classes use CSS Modules (`styles['class-name']`), not inline styles or global classNames +- [ ] No new UI or state management libraries added (`react-intl`, `react-split-pane`, `react-table` are the approved set) +- [ ] No hardcoded user-visible text (all strings use `` or `intl.formatMessage()`) + +### 3. Localization +- [ ] Every new user-facing string has an entry in `src/locales/en.json` +- [ ] The same key exists in all 13 other locale files (cs, de, es, fr, it, ja, ko, pl, pt-BR, ru, zh-Hans, zh-Hant) +- [ ] Key format follows `module.element.descriptor` convention (e.g., `recent.table.header.name`) +- [ ] New locales added to `src/localization/localization.ts` in `getMessagesForLocale()` + +### 4. Unit tests +- [ ] Modified or new components have corresponding test files in `tests/unit/` +- [ ] Tests assert behavior (what renders, what happens on click), not implementation details +- [ ] Chrome WebView globals are not re-mocked inline — they come from `tests/__mocks__/chromeMock.ts` +- [ ] Coverage not reduced for modified files + +### 5. E2E tests +- [ ] Playwright tests use Page Object Model — no selectors or `page.locator()` calls in `tests/e2e/e2e.test.ts` +- [ ] Page/Component classes live in `tests/e2e/pages/` or `tests/e2e/components/` +- [ ] New user flows have e2e coverage (or a TODO with justification) + +### 6. Build and dependencies +- [ ] No new entries in `dependencies` or `devDependencies` without clear justification +- [ ] `npm run lint:check` passes (verify or ask the author to confirm) +- [ ] `npm run build` still produces a valid bundle + +### 7. Backend integration +- [ ] `window.chrome?.webview` is always guarded before access (check for optional chaining) +- [ ] New backend calls go through `src/functions/utility.ts`, not inline in components +- [ ] Dev fallback (mock data from `src/assets/`) preserved for any new data flow + +### 8. Architecture +- [ ] New components placed in the correct module folder (`Common/`, `Recent/`, `Samples/`, `Learning/`, `Sidebar/`) +- [ ] Shared components go in `Common/` if used by more than one module +- [ ] No business logic in `index.tsx` or `App.tsx` + +## Feedback guidelines + +- Flag **blockers** (will break functionality, regression risk, missing tests) clearly and explain why +- Flag **recommendations** (better approach, small improvement) separately and mark them optional +- Do **not** request refactors of code not touched by the PR +- Do **not** flag cosmetic style differences if they follow the existing codebase pattern +- Be specific: reference file and line numbers, provide corrected code snippets when helpful +- Keep feedback concise — one issue per comment, actionable language + +## Dynamo-specific concerns + +- The app runs inside a Chrome WebView — DOM APIs and browser behavior may differ from a normal browser +- `window.setLocale()`, `window.receiveGraphDataFromDotNet()`, and similar globals are called by the Dynamo host — changes to their signatures or behavior are breaking changes +- The output bundle path `dist/build/index.bundle.js` must not change — Dynamo hardcodes this path +- View modes (`recentPageViewMode`, `samplesViewMode`) are persisted to the Dynamo backend via `saveHomePageSettings()` — verify settings round-trip still works after state changes diff --git a/.claude/agents/frontend-agent.md b/.claude/agents/frontend-agent.md new file mode 100644 index 0000000..685fa24 --- /dev/null +++ b/.claude/agents/frontend-agent.md @@ -0,0 +1,89 @@ +--- +name: frontend-agent +description: Use when implementing or modifying React components, adding UI features, fixing visual bugs, updating localized strings, or writing unit tests. Covers all work inside src/components/, src/locales/, and src/localization/. +model: claude-sonnet-4-6 +tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep +--- + +You are a Senior React + TypeScript Engineer working on DynamoHome, a React 18 SPA that serves as the landing page for Dynamo (an Autodesk visual programming tool). The app runs inside a Chrome WebView embedded in the Dynamo desktop application. + +## Project structure you own + +``` +src/ + components/ + Common/ # Shared components (CardItem, Tooltip, Arrow, Portal, CustomIcons) + Recent/ # Recent files module (PageRecent, GraphTable, GraphGridItem, cell renderers) + Samples/ # Sample graphs module (PageSamples, SamplesGrid, SamplesTable) + Learning/ # Learning resources (PageLearning, Carousel, GuideGridItem, VideoCarouselItem) + Sidebar/ # Navigation (Sidebar, CustomDropDown) + LayoutContainer.tsx + MainContent.tsx + SettingsContext.tsx + locales/ # 14 JSON translation files (en, es, de, fr, it, ja, ko, cs, pl, pt-BR, ru, zh-Hans, zh-Hant) + localization/ + localization.ts # getMessagesForLocale() maps locale string → JSON messages + functions/ + utility.ts # Backend integration functions (calls window.chrome.webview.hostObjects.scriptObject) + assets/ # Dev-only mock data (home.ts, samples.ts, learning.ts) +``` + +## Component rules + +- **Functional components only** — no class components +- **TypeScript required** — define explicit prop interfaces; never use `any` for props or state +- **CSS Modules** — all styles go in `ComponentName.module.css`; import as `import styles from './ComponentName.module.css'`; access as `styles['class-name']` +- **No new libraries** — React, react-intl, react-split-pane, react-table are the approved UI libraries; do not add others +- **Reuse before creating** — check `src/components/Common/` before building a new shared component +- **No hardcoded text** — every user-facing string must use `` from react-intl; no exceptions + +## Localization rules + +- All strings live in `src/locales/en.json` (source of truth) and must be duplicated to all 13 other locale files +- Key format: `module.element.descriptor` — e.g., `recent.table.header.name`, `button.title.text.open` +- Use `` in JSX +- Use `intl.formatMessage({ id: 'your.key' })` when a string value is needed (not JSX), obtained from `useIntl()` hook +- To add a new locale: add the JSON file to `src/locales/`, add the mapping in `src/localization/localization.ts` inside `getMessagesForLocale()` + +## State and data patterns + +- **Context** for shared settings: `SettingsContext` exposes `recentPageViewMode`, `samplesViewMode`, `sideBarWidth`; consume via `useSettings()` hook +- **Local state** via `useState` for component-level UI state +- **Backend data** arrives via global callbacks: `window.receiveGraphDataFromDotNet`, `window.receiveSamplesDataFromDotNet`, `window.receiveTrainingVideoDataFromDotNet`, `window.receiveInteractiveGuidesDataFromDotNet` +- **Dev mode**: check `window.chrome?.webview` — if missing, use mock data from `src/assets/` +- **Never** add Redux, Zustand, MobX, or any external state library + +## Unit testing rules + +- Every component you create or modify **must** have a test file in `tests/unit/` +- Framework: Jest 29 + `@testing-library/react` 15 +- Run tests: `npm run test:unit` +- Chrome WebView globals are mocked in `tests/__mocks__/chromeMock.ts` (auto-applied via `tests/jest.setup.ts`) +- CSS modules mocked via `identity-obj-proxy`; images mocked via `tests/__mocks__/fileMock.ts` +- Test behavior, not implementation: assert what the user sees/does, not internal state +- 100% branch coverage target for files you modify + +## Commands to use + +```bash +npm run lint:check # Check for lint errors +npm run lint:fix # Auto-fix lint errors +npm run test:unit # Run unit tests +npm run build # Dev build (webpack, unminified) +npm run start # Dev server on port 8080 +``` + +## What NOT to do + +- Do not introduce new npm dependencies without explicit user approval +- Do not use class components +- Do not hardcode user-visible text in any language +- Do not modify `webpack.config.ts`, `jest.config.ts`, or Playwright configuration +- Do not modify `src/functions/utility.ts` unless fixing a bug in backend integration +- Do not refactor unrelated code while implementing a feature diff --git a/.claude/agents/testing-agent.md b/.claude/agents/testing-agent.md new file mode 100644 index 0000000..b8fe6b5 --- /dev/null +++ b/.claude/agents/testing-agent.md @@ -0,0 +1,135 @@ +--- +name: testing-agent +description: Use when creating or maintaining end-to-end Playwright tests, implementing Page Object Model classes, or investigating test failures in tests/e2e.test.ts. +model: claude-sonnet-4-6 +tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep +--- + +You are an End-to-End Test Engineer working on DynamoHome, a React 18 SPA running inside a Chrome WebView in the Dynamo desktop application. You own all Playwright e2e tests. + +## Test infrastructure + +``` +tests/ + unit/ # Unit tests (Jest) + App.test.tsx + e2e/ # End-to-end tests (Playwright) + e2e.test.ts # Orchestration only — no selectors here + pages/ # Page Object classes + components/ # Component Object classes + jest.setup.ts # Unit test setup — applies chrome global mock + __mocks__/ + chromeMock.ts # Mock for window.chrome.webview globals + fileMock.ts # Mock for image imports + styleMock.ts # Mock for CSS module imports + +playwright.config.js # Config: testDir=./tests/e2e, Chromium only, port 8080, 30s timeout, 2 retries on CI +``` + +Run e2e tests: `npm run test:e2e` +Start dev server first if running locally: `npm run start` (serves on `http://localhost:8080`) + +## Page Object Model — mandatory structure + +Every test must follow strict POM. Create files alongside tests in `tests/`: + +``` +tests/ + e2e/ + e2e.test.ts # Orchestration only — no selectors or page actions here + pages/ + RecentPage.ts # Represents the Recent files page + SamplesPage.ts # Represents the Samples page + LearningPage.ts # Represents the Learning page + components/ + Sidebar.ts # Sidebar navigation component class + CardItem.ts # Shared card grid item + GraphTable.ts # Table component (Recent/Samples) + Carousel.ts # Carousel component (Learning) + unit/ + App.test.tsx # Unit tests (Jest) + jest.setup.ts # Applied globally via jest.config.ts + __mocks__/ # Auto-applied mocks +``` + +### Page class pattern + +```typescript +import { Page } from '@playwright/test'; + +export class RecentPage { + constructor(private page: Page) {} + + // All selectors defined here — NEVER in test files + private graphGridItems = () => this.page.locator('[data-testid="graph-grid-item"]'); + private listViewToggle = () => this.page.locator('[data-testid="list-view-toggle"]'); + + // All actions defined here — NEVER in test files + async switchToListView() { + await this.listViewToggle().click(); + } + + async getGraphCount(): Promise { + return this.graphGridItems().count(); + } +} +``` + +### Test file pattern + +```typescript +import { test, expect } from '@playwright/test'; +import { RecentPage } from './pages/RecentPage'; + +test('recent page displays graphs in grid view by default', async ({ page }) => { + const recentPage = new RecentPage(page); + await page.goto('http://localhost:8080'); + const count = await recentPage.getGraphCount(); + expect(count).toBeGreaterThan(0); +}); +``` + +## Hard rules + +- **Test files must not contain selectors** (no `.locator()`, no `page.$()`, no `data-testid` strings in test files) +- **Test files must not contain direct Playwright actions** (no `await page.click()`, `await page.fill()` in test files) +- **All selectors live in Page or Component classes** +- **All actions live in Page or Component classes** +- Prefer `data-testid` attributes for selectors; if missing, add them to the component via the frontend-agent +- Tests must be deterministic — avoid `waitForTimeout()`; use `waitForSelector()` or expect-based waiting instead + +## Application pages to cover + +The app has three main pages switchable via the Sidebar: +1. **Recent** — shows recent Dynamo files in grid or list (table) view; supports open/delete/pin actions +2. **Samples** — shows sample graphs in grid or list view +3. **Learning** — shows guides carousel and video carousel + +The Sidebar is a fixed left panel (resizable via SplitPane). Navigation is handled by clicking sidebar items. + +## Backend integration in tests + +The app uses `window.chrome.webview.hostObjects.scriptObject` for all backend calls. In test environment (dev mode), the app falls back to mock data from `src/assets/`. Tests run against the dev server (`npm start`) which serves mock data automatically — no mocking needed in e2e tests. + +## Commands + +```bash +npm run test:e2e # Run all Playwright tests +npm run start # Start dev server (required before running e2e locally) +npx playwright show-report # View HTML test report after run +playwright-cli open http://localhost:8080 # Exploratory testing / selector discovery +``` + +## What NOT to do + +- Do not place selectors or `page.locator()` calls inside `e2e.test.ts` +- Do not use `waitForTimeout()` or arbitrary sleeps +- Do not write tests that depend on network calls to the real Dynamo backend +- Do not modify unit test files (`App.test.tsx`, `jest.setup.ts`) +- Do not modify `jest.config.ts` or `playwright.config.js` without user approval diff --git a/.claude/knowledge/architecture.md b/.claude/knowledge/architecture.md new file mode 100644 index 0000000..e464681 --- /dev/null +++ b/.claude/knowledge/architecture.md @@ -0,0 +1,89 @@ +# Architecture – DynamoHome + +DynamoHome is a **React 18 single-page application** that serves as the home/start page for Dynamo, an Autodesk visual programming tool. It runs inside a **Chrome WebView** embedded in the Dynamo desktop application — not in a standalone browser. + +## Component tree + +``` +App.tsx # IntlProvider (localization) + SettingsProvider (context) +└── LayoutContainer.tsx # SplitPane: resizable sidebar + main content + ├── Sidebar.tsx # Left nav panel — page switching + custom dropdowns + │ └── CustomDropDown.tsx + └── MainContent.tsx # Renders active page based on sidebar selection + ├── PageRecent.tsx # Recent Dynamo files — grid or table view + │ ├── GraphGridItem.tsx + │ └── GraphTable.tsx # react-table with custom cell renderers + │ ├── CustomNameCellRenderer.tsx + │ ├── CustomLocationCellRenderer.tsx + │ └── CustomAuthorCellRenderer.tsx + ├── PageSamples.tsx # Sample graphs — grid or table view + │ ├── SamplesGrid.tsx + │ │ └── SamplesGridItem.tsx + │ └── SamplesTable.tsx + │ └── CustomSampleFirstCellRenderer.tsx + └── PageLearning.tsx # Learning resources — guides + video carousels + ├── Carousel.tsx + ├── GuideGridItem.tsx + ├── ModalItem.tsx + └── VideoCarouselItem.tsx + +Common/ # Shared across modules + CardItem.tsx # Reusable card for grid views + Tooltip.tsx + Arrow.tsx + Portal.tsx + CustomIcons.tsx # SVG icons: GridView, ListView +``` + +## Data flow + +``` +Dynamo backend (.NET) + │ + ▼ calls window globals +window.receiveGraphDataFromDotNet(json) → PageRecent state +window.receiveSamplesDataFromDotNet(json) → PageSamples state +window.receiveTrainingVideoDataFromDotNet(json) → PageLearning state +window.receiveInteractiveGuidesDataFromDotNet(json) → PageLearning state + │ + ▼ +Component setState → React render + │ + ▼ user interaction +window.chrome.webview.hostObjects.scriptObject.OpenFile(path) +window.chrome.webview.hostObjects.scriptObject.DeleteFile(path) +window.chrome.webview.hostObjects.scriptObject.OpenUrl(url) + │ + ▼ +Back to Dynamo +``` + +In **development mode** (no WebView), the app detects `!window.chrome?.webview` and loads mock data from `src/assets/home.ts`, `samples.ts`, `learning.ts`. + +## Settings persistence + +`SettingsContext.tsx` holds: `recentPageViewMode`, `samplesViewMode`, `sideBarWidth` + +- Loaded on init via `window.chrome.webview.hostObjects.scriptObject.GetHomePageSettings()` +- Saved on change via `saveHomePageSettings()` in `src/functions/utility.ts` +- Consumed via the `useSettings()` custom hook + +## Localization + +- `App.tsx` wraps the tree with `` +- Locale is set at runtime by Dynamo calling `window.setLocale(locale)` +- Messages come from `src/localization/localization.ts → getMessagesForLocale(locale)` +- 14 locales: en, cs, de, es, fr, it, ja, ko, pl, pt-BR, ru, zh-Hans, zh-Hant + +## Build output + +- **Entry**: `src/index.tsx` +- **Output**: `dist/build/index.bundle.js` + `dist/build/index.html` +- Dynamo hardcodes the path `dist/build/index.bundle.js` — do not change it + +## Key constraints + +- No backend server — all data comes from the Dynamo host via WebView +- No routing library — page switching is managed by component state in `MainContent.tsx` +- No Redux or external state management — React Context + useState only +- The app must work in Chromium (WebView) — no reliance on Firefox/Safari-specific APIs diff --git a/.claude/knowledge/domain-dynamo.md b/.claude/knowledge/domain-dynamo.md new file mode 100644 index 0000000..6e24f6d --- /dev/null +++ b/.claude/knowledge/domain-dynamo.md @@ -0,0 +1,86 @@ +# Dynamo Domain Knowledge + +## What Dynamo is + +Dynamo is a visual programming tool by Autodesk used in architecture, engineering, and construction. DynamoHome is its start page — a webview-based UI embedded in the Dynamo desktop application. + +## How the host integration works + +The app communicates with the Dynamo host (.NET) exclusively through `window.chrome.webview.hostObjects.scriptObject`. This object is injected by the WebView host and exposes backend methods. + +### Methods the app calls on Dynamo + +```typescript +scriptObject.OpenFile(filePath: string) // Open a Dynamo graph file +scriptObject.DeleteFile(filePath: string) // Delete a recent file entry +scriptObject.OpenUrl(url: string) // Open URL in browser +scriptObject.GetHomePageSettings(): string // Returns JSON settings string +scriptObject.SaveHomePageSettings(json: string) // Persist user preferences +scriptObject.PinGraph(filePath: string) // Pin/unpin recent file +``` + +All of these are async (return promises) and must be called with `await`. + +### Global callbacks Dynamo calls on the app + +These are set on `window` and called by the .NET host: + +```typescript +window.receiveGraphDataFromDotNet(json: string) // Recent files data +window.receiveSamplesDataFromDotNet(json: string) // Sample graphs data +window.receiveTrainingVideoDataFromDotNet(json: string) // Learning videos data +window.receiveInteractiveGuidesDataFromDotNet(json: string) // Learning guides data +window.setLocale(locale: string) // Change UI language +window.setShowStartPageChanged(show: boolean) // Loading overlay control +window.setHomePageSettings(settingsJson: string) // Apply persisted settings +``` + +**Never rename or remove these globals** — Dynamo calls them by name from .NET code. Any signature change is a breaking change. + +## Data shapes + +Recent file entry (from `receiveGraphDataFromDotNet`): +```json +{ + "Name": "MyGraph.dyn", + "Path": "C:\\Users\\user\\Documents\\MyGraph.dyn", + "Author": "user@company.com", + "TimeStamp": "2024-01-15T10:30:00", + "IsPinned": false +} +``` + +Sample graph entry: +```json +{ + "Name": "Sample Graph", + "Description": "A sample Dynamo graph", + "Path": "C:\\Program Files\\Dynamo\\samples\\...", + "ImagePath": "relative/path/to/image.png" +} +``` + +Settings JSON (persisted to Dynamo): +```json +{ + "recentPageViewMode": "grid", + "samplesViewMode": "list", + "sideBarWidth": 200 +} +``` + +## Development mode + +When `window.chrome?.webview` is not present (running via `npm start` outside Dynamo), the app uses mock data from: +- `src/assets/home.ts` — mock recent files +- `src/assets/samples.ts` — mock samples +- `src/assets/learning.ts` — mock learning content + +This allows full development without Dynamo installed. + +## Compatibility constraints + +- The output bundle path `dist/build/index.bundle.js` is hardcoded in Dynamo — never change it +- The app must run in Chromium (Edge WebView2) — no reliance on browser-specific APIs not in Chromium +- Locale identifiers must match what Dynamo sends (e.g., `"en-US"`, `"de-DE"`, `"zh-Hans"`) — check `src/localization/localization.ts` for the full mapping +- Settings schema is shared with Dynamo — adding new fields is safe; renaming/removing existing fields is a breaking change diff --git a/.claude/knowledge/project-conventions.md b/.claude/knowledge/project-conventions.md new file mode 100644 index 0000000..caf3099 --- /dev/null +++ b/.claude/knowledge/project-conventions.md @@ -0,0 +1,92 @@ +# Project Conventions – DynamoHome + +## File and folder structure + +- Components live in `src/components/[Module]/ComponentName.tsx` +- Styles live in `src/components/[Module]/ComponentName.module.css` (CSS Modules, same folder as component) +- Shared components (used by 2+ modules) go in `src/components/Common/` +- Unit tests live in `tests/ComponentName.test.tsx` (flat, not mirrored structure) +- E2E page objects live in `tests/pages/` and `tests/components/` +- Locale strings live in `src/locales/[locale].json` — all 14 files must stay in sync + +## Naming conventions + +- **Components**: PascalCase (`GraphGridItem.tsx`, `CustomDropDown.tsx`) +- **CSS Module files**: same name as component (`GraphGridItem.module.css`) +- **Hooks**: camelCase prefixed with `use` (`useSettings`) +- **Type interfaces**: PascalCase, descriptive (`SidebarItem`, `HomePageSetting`) +- **Locale keys**: dot-notation, `module.element.descriptor` (`recent.table.header.name`, `button.title.text.open`) +- **Test files**: `ComponentName.test.tsx` for unit, `e2e.test.ts` for e2e + +## Component conventions + +```tsx +// ✅ Correct: explicit prop interface, CSS Modules, FormattedMessage +interface CardItemProps { + imageSrc: string; + onClick: () => void; + titleText: string; +} + +export const CardItem = ({ imageSrc, onClick, titleText }: CardItemProps) => { + return ( +
+ +
+ ); +}; + +// ❌ Wrong: inline types, hardcoded text, any type +export const CardItem = ({ imageSrc, onClick }: { imageSrc: any; onClick: any }) => ( +
Recent Files
+); +``` + +## CSS Modules convention + +```tsx +import styles from './CardItem.module.css'; + +// Access with bracket notation (kebab-case class names) +
+``` + +## Localization convention + +```tsx +// In JSX — use FormattedMessage + + +// In props or attributes — use useIntl hook +const intl = useIntl(); + +``` + +## Backend integration convention + +All calls to Dynamo backend go through `src/functions/utility.ts`: +```tsx +// ✅ Correct: use utility functions +import { openFile, saveHomePageSettings } from '../functions/utility'; + +// ❌ Wrong: inline webview calls in components +window.chrome.webview.hostObjects.scriptObject.OpenFile(path); +``` + +Always guard WebView access: +```tsx +// ✅ Correct +if (window.chrome?.webview) { + await scriptObject.OpenFile(path); +} else { + console.log('[DEV] OpenFile:', path); // dev fallback +} +``` + +## Scope discipline + +- Make the **smallest change** that satisfies the requirement +- Do not refactor adjacent code while implementing a feature +- Do not add dependencies without explicit user approval +- Do not change build config, test config, or CI pipelines as a side effect +- If you notice a bug or improvement opportunity outside your task scope, mention it — don't fix it unilaterally diff --git a/.claude/knowledge/stack.md b/.claude/knowledge/stack.md new file mode 100644 index 0000000..1de6e8d --- /dev/null +++ b/.claude/knowledge/stack.md @@ -0,0 +1,78 @@ +# Technology Stack – DynamoHome + +## Core + +| Technology | Version | Purpose | +|---|---|---| +| React | ^18.2.0 | UI framework | +| React DOM | ^18.2.0 | DOM rendering via `createRoot` | +| TypeScript | ^5.4.5 | Type safety | + +## Build + +| Technology | Version | Purpose | +|---|---|---| +| Webpack | ^5.92.0 | Module bundler | +| Webpack CLI | ^5.1.4 | CLI runner | +| Webpack Dev Server | ^5.2.2 | Dev server, port 8080, hot reload | +| ts-loader | ^9.5.1 | TypeScript → Webpack | +| babel-loader | ^9.1.3 | JS/JSX → Webpack | +| Babel Core | ^7.23.5 | JS transpilation | +| css-loader | ^6.8.1 | CSS imports | +| style-loader | ^3.3.3 | Injects CSS into DOM | +| HtmlWebpackPlugin | ^5.5.4 | Generates index.html | +| TerserPlugin | bundled | Production minification | + +## UI Libraries + +| Library | Version | Purpose | +|---|---|---| +| react-intl | ^6.6.1 | Localization (FormattedMessage, useIntl) | +| react-split-pane | ^0.1.92 | Resizable sidebar/content split | +| react-table | ^7.8.0 | Headless table (Recent, Samples views) | + +## Testing + +| Technology | Version | Purpose | +|---|---|---| +| Jest | ^29.7.0 | Unit test runner | +| ts-jest | ^29.1.5 | TypeScript support in Jest | +| @testing-library/react | ^15.0.6 | Component testing utilities | +| @testing-library/dom | ^10.3.0 | DOM testing utilities | +| jest-environment-jsdom | ^29.7.0 | DOM simulation for unit tests | +| identity-obj-proxy | ^3.0.0 | CSS Modules mock in Jest | +| @types/jest | ^29.5.12 | Jest type definitions | +| Playwright | ^1.27.1 | E2E test runner | +| @playwright/test | ^1.27.1 | Playwright test framework | + +## Code Quality + +| Technology | Version | Purpose | +|---|---|---| +| ESLint | ^8.57.0 | Linting | +| eslint-plugin-react | ^7.34.1 | React-specific lint rules | + +## Key npm scripts + +```bash +npm run start # Dev server (webpack-dev-server, port 8080) +npm run build # Dev bundle (unminified) +npm run bundle # Production bundle (minified) +npm run production # bundle + copy metadata to dist/ +npm run test:unit # Jest unit tests +npm run test:e2e # Playwright e2e tests +npm run test # test:unit + test:e2e +npm run lint:check # ESLint check (read-only) +npm run lint:fix # ESLint auto-fix +``` + +## TypeScript config notes + +- `strict: false` — intentional; do not enable without user approval +- `target: "ES2016"`, `module: "CommonJS"`, `jsx: "react-jsx"` +- `resolveJsonModule: true` — needed for locale JSON imports +- Custom type roots: `./types/index.d.ts` for global declarations (Window extensions, Locale type) + +## Supported locales + +en, cs, de, es, fr, it, ja, ko, pl, pt-BR, ru, zh-Hans, zh-Hant (14 total) diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..a06d4a3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(find /c/repos/DynamoHome/.claude -type f -name *.md -o -name *.json -o -name *.yaml -o -name *.yml)", + "Bash(npx jest:*)" + ] + } +} diff --git a/.claude/skills/build-tooling.md b/.claude/skills/build-tooling.md new file mode 100644 index 0000000..c1096cf --- /dev/null +++ b/.claude/skills/build-tooling.md @@ -0,0 +1,74 @@ +# Build and Tooling Skills – DynamoHome + +## Bundler: Webpack 5 + +`webpack.config.ts` is the single source of truth for all bundling. + +### Key configuration + +``` +Entry: src/index.tsx +Output: dist/build/index.bundle.js ← path is hardcoded in Dynamo, do NOT change +Loaders: ts-loader (TS/TSX), babel-loader (JS/JSX), css-loader + style-loader (CSS) +Plugins: HtmlWebpackPlugin → dist/build/index.html +Modes: development (unminified) | production (TerserPlugin minification) +``` + +### Build commands + +```bash +npm run start # webpack-dev-server, port 8080, hot reload +npm run build # dev bundle (webpack --mode=development) +npm run bundle # production bundle (webpack --mode=production, minified) +npm run production # bundle + copy package.json, README.md, license_output → dist/ +``` + +## TypeScript: tsconfig.json + +Critical settings to preserve: +- `"strict": false` — enabling this breaks existing code; do not change without explicit user approval +- `"resolveJsonModule": true` — required for `import messages from './locales/en.json'` +- `"typeRoots": ["./types", "./node_modules/@types"]` — custom types loaded from `types/index.d.ts` +- `"jsx": "react-jsx"` — no need for `import React from 'react'` in components + +## ESLint: .eslintrc + +- Extends `react-app` preset +- Covers `src/` and `tests/` +- Run: `npm run lint:check` (read-only), `npm run lint:fix` (auto-fix) +- Must pass before any PR merge + +## Jest: jest.config.ts + +- Preset: `ts-jest` +- Environment: `jsdom` +- Coverage from: `src/**/*.{ts,tsx}`, output to `./coverage/` +- Setup file: `tests/jest.setup.ts` — applies chrome global mocks +- Module name mapper: CSS → `identity-obj-proxy`, images → `fileMock.ts` +- Unit test files live in: `tests/unit/` +- Run: `npm run test:unit` + +## Playwright: playwright.config.js + +- Chromium only +- `testDir: './tests/e2e'` — scoped to e2e tests only +- Web server: auto-starts `npm start` before tests, waits for port 8080 +- CI: 1 worker, 2 retries; local: unlimited workers, 0 retries +- Run: `npm run test:e2e` (targets `tests/e2e/e2e.test.ts`) + +## When making build changes + +1. After `webpack.config.ts` changes: verify `npm run build` AND `npm run bundle` both succeed +2. After `tsconfig.json` changes: verify `npm run lint:check` and `npm run test:unit` pass +3. After `package.json` script changes: verify all affected scripts still work +4. After adding a new webpack loader: verify CSS Modules still work (`identity-obj-proxy` in tests), and that the production build minifies correctly +5. After CI changes (`.github/workflows/`): review that `build.yml` triggers correctly on PR and push + +## What NOT to do + +- Do not switch to Vite, Rollup, esbuild, or Parcel +- Do not change the output path `dist/build/` — Dynamo integration depends on it +- Do not enable `strict: true` in `tsconfig.json` +- Do not rename existing npm scripts — CI pipelines call them by name +- Do not add loaders or plugins for technologies not used in the project (e.g., SASS, Less, SVGR) without explicit approval +- Do not modify `jest.setup.ts` or the mock files in `tests/__mocks__/` without understanding downstream impact diff --git a/.claude/skills/code-review.md b/.claude/skills/code-review.md new file mode 100644 index 0000000..2bb0f2f --- /dev/null +++ b/.claude/skills/code-review.md @@ -0,0 +1,63 @@ +# Code Review Skills – DynamoHome + +## Review mindset + +Focus on real problems: regressions, missing tests, broken localization, type safety gaps, Dynamo integration breakage. Do not request style changes or refactors for code not touched by the PR. Keep feedback concise and actionable. + +## Checklist + +### TypeScript +- [ ] No new `any` types introduced for props, state, or function parameters +- [ ] No `// @ts-ignore` added +- [ ] New types are interfaces (not inline) if used in more than one place +- [ ] Existing `any` in legacy code (`SettingsContext`, `utility.ts`) is not flagged as new issues + +### React components +- [ ] Functional components only (no class components) +- [ ] Explicit prop interfaces defined +- [ ] CSS classes use `styles['class-name']` from CSS Modules imports — no global classNames or inline styles +- [ ] No new UI or state management libraries added to `package.json` + +### Localization +- [ ] Every new user-visible string has a key in `src/locales/en.json` +- [ ] The same key exists in all 13 other locale files (`cs`, `de`, `es`, `fr`, `it`, `ja`, `ko`, `pl`, `pt-BR`, `ru`, `zh-Hans`, `zh-Hant`) +- [ ] Keys follow `module.element.descriptor` format +- [ ] No hardcoded text in JSX (no raw string children, no `title=""` without intl) + +### Unit tests +- [ ] New or modified components have test files in `tests/` +- [ ] Tests validate behavior (user sees X, clicking Y does Z), not implementation details +- [ ] No re-mocking of globals already provided by `tests/__mocks__/chromeMock.ts` +- [ ] Coverage not reduced for modified files + +### E2E tests +- [ ] Playwright tests have no selectors in `e2e.test.ts` (all in Page/Component classes) +- [ ] No `page.locator()` or `page.click()` calls directly in test files +- [ ] No `waitForTimeout()` calls + +### Dynamo integration +- [ ] `window.chrome?.webview` is guarded with optional chaining before access +- [ ] No global callback functions renamed or removed (`receiveGraphDataFromDotNet`, `setLocale`, etc.) +- [ ] Settings JSON shape unchanged (no field removals or renames) +- [ ] Output bundle path `dist/build/index.bundle.js` not changed + +### Dependencies and build +- [ ] No new `npm` packages without justification +- [ ] `npm run lint:check` passes +- [ ] `npm run build` produces a valid bundle +- [ ] Existing npm script names unchanged + +## How to give feedback + +**Blocker** (must fix before merge): +> `src/components/Recent/GraphGridItem.tsx:42` — `title` prop is hardcoded `"Open File"`. Must use ``. Add the key to all 14 locale files. + +**Recommendation** (optional improvement): +> Consider extracting the `formatDate()` call on line 28 to `utility.ts` since the same logic appears in `CustomNameCellRenderer.tsx`. Not blocking. + +## What NOT to flag + +- Existing `any` types in files not touched by the PR +- Code style differences that match the surrounding file (unless they introduce bugs) +- Missing tests for components that already existed before the PR +- Architecture concerns outside the scope of the change diff --git a/.claude/skills/localization.md b/.claude/skills/localization.md new file mode 100644 index 0000000..19179dc --- /dev/null +++ b/.claude/skills/localization.md @@ -0,0 +1,82 @@ +# Localization Skills – DynamoHome + +## System overview + +- Library: **react-intl** (v6) +- Source of truth: `src/locales/en.json` +- All 14 locale files must stay in sync: `en, cs, de, es, fr, it, ja, ko, pl, pt-BR, ru, zh-Hans, zh-Hant` +- Locale is set at runtime by Dynamo via `window.setLocale(locale)` +- Mapping from locale string → messages: `src/localization/localization.ts → getMessagesForLocale()` + +## Key naming convention + +Format: `module.element.descriptor` + +```json +// ✅ Good key names +"recent.table.header.name": "Name", +"recent.table.header.location": "Location", +"samples.grid.item.open": "Open Sample", +"learning.carousel.title.guides": "Interactive Guides", +"button.title.text.open": "Open", +"sidebar.item.recent": "Recent" + +// ❌ Bad key names +"openButton": "Open", +"name": "Name", +"recentTableHeaderName": "Name" +``` + +## Adding a new string — required steps + +**Step 1:** Add the key to `src/locales/en.json`: +```json +{ + "recent.filter.placeholder": "Search files..." +} +``` + +**Step 2:** Add the same key to all 13 other locale files (`cs.json`, `de.json`, `es.json`, `fr.json`, `it.json`, `ja.json`, `ko.json`, `pl.json`, `pt-BR.json`, `ru.json`, `zh-Hans.json`, `zh-Hant.json`). Use the English value as a placeholder if translations are not yet available: +```json +{ + "recent.filter.placeholder": "Search files..." +} +``` + +**Step 3:** Use the key in your component: + +```tsx +// In JSX — use FormattedMessage +import { FormattedMessage } from 'react-intl'; + + +// In attributes/props — use useIntl hook +import { useIntl } from 'react-intl'; +const intl = useIntl(); + +``` + +## Adding a new locale + +1. Add `src/locales/[locale].json` with all existing keys translated +2. Add the mapping in `src/localization/localization.ts`: +```typescript +case 'pt-PT': + return import('./locales/pt-PT.json'); +``` +3. Add the locale to the `Locale` type in `types/index.d.ts` if needed + +## What NOT to do + +```tsx +// ❌ Never hardcode user-visible text + +
+ +// ❌ Never use string concatenation with locale strings +const msg = intl.formatMessage({ id: 'prefix' }) + name; +// ✅ Use FormattedMessage with values prop instead + + +// ❌ Never add a key to en.json without adding it to all other locale files +``` diff --git a/.claude/skills/playwright-cli/SKILL.md b/.claude/skills/playwright-cli/SKILL.md new file mode 100644 index 0000000..11bad2b --- /dev/null +++ b/.claude/skills/playwright-cli/SKILL.md @@ -0,0 +1,278 @@ +--- +name: playwright-cli +description: Automates browser interactions for web testing, form filling, screenshots, and data extraction. Use when the user needs to navigate websites, interact with web pages, fill forms, take screenshots, test web applications, or extract information from web pages. +allowed-tools: Bash(playwright-cli:*) +--- + +# Browser Automation with playwright-cli + +## Quick start + +```bash +# open new browser +playwright-cli open +# navigate to a page +playwright-cli goto https://playwright.dev +# interact with the page using refs from the snapshot +playwright-cli click e15 +playwright-cli type "page.click" +playwright-cli press Enter +# take a screenshot (rarely used, as snapshot is more common) +playwright-cli screenshot +# close the browser +playwright-cli close +``` + +## Commands + +### Core + +```bash +playwright-cli open +# open and navigate right away +playwright-cli open https://example.com/ +playwright-cli goto https://playwright.dev +playwright-cli type "search query" +playwright-cli click e3 +playwright-cli dblclick e7 +playwright-cli fill e5 "user@example.com" +playwright-cli drag e2 e8 +playwright-cli hover e4 +playwright-cli select e9 "option-value" +playwright-cli upload ./document.pdf +playwright-cli check e12 +playwright-cli uncheck e12 +playwright-cli snapshot +playwright-cli snapshot --filename=after-click.yaml +playwright-cli eval "document.title" +playwright-cli eval "el => el.textContent" e5 +playwright-cli dialog-accept +playwright-cli dialog-accept "confirmation text" +playwright-cli dialog-dismiss +playwright-cli resize 1920 1080 +playwright-cli close +``` + +### Navigation + +```bash +playwright-cli go-back +playwright-cli go-forward +playwright-cli reload +``` + +### Keyboard + +```bash +playwright-cli press Enter +playwright-cli press ArrowDown +playwright-cli keydown Shift +playwright-cli keyup Shift +``` + +### Mouse + +```bash +playwright-cli mousemove 150 300 +playwright-cli mousedown +playwright-cli mousedown right +playwright-cli mouseup +playwright-cli mouseup right +playwright-cli mousewheel 0 100 +``` + +### Save as + +```bash +playwright-cli screenshot +playwright-cli screenshot e5 +playwright-cli screenshot --filename=page.png +playwright-cli pdf --filename=page.pdf +``` + +### Tabs + +```bash +playwright-cli tab-list +playwright-cli tab-new +playwright-cli tab-new https://example.com/page +playwright-cli tab-close +playwright-cli tab-close 2 +playwright-cli tab-select 0 +``` + +### Storage + +```bash +playwright-cli state-save +playwright-cli state-save auth.json +playwright-cli state-load auth.json + +# Cookies +playwright-cli cookie-list +playwright-cli cookie-list --domain=example.com +playwright-cli cookie-get session_id +playwright-cli cookie-set session_id abc123 +playwright-cli cookie-set session_id abc123 --domain=example.com --httpOnly --secure +playwright-cli cookie-delete session_id +playwright-cli cookie-clear + +# LocalStorage +playwright-cli localstorage-list +playwright-cli localstorage-get theme +playwright-cli localstorage-set theme dark +playwright-cli localstorage-delete theme +playwright-cli localstorage-clear + +# SessionStorage +playwright-cli sessionstorage-list +playwright-cli sessionstorage-get step +playwright-cli sessionstorage-set step 3 +playwright-cli sessionstorage-delete step +playwright-cli sessionstorage-clear +``` + +### Network + +```bash +playwright-cli route "**/*.jpg" --status=404 +playwright-cli route "https://api.example.com/**" --body='{"mock": true}' +playwright-cli route-list +playwright-cli unroute "**/*.jpg" +playwright-cli unroute +``` + +### DevTools + +```bash +playwright-cli console +playwright-cli console warning +playwright-cli network +playwright-cli run-code "async page => await page.context().grantPermissions(['geolocation'])" +playwright-cli tracing-start +playwright-cli tracing-stop +playwright-cli video-start +playwright-cli video-stop video.webm +``` + +## Open parameters +```bash +# Use specific browser when creating session +playwright-cli open --browser=chrome +playwright-cli open --browser=firefox +playwright-cli open --browser=webkit +playwright-cli open --browser=msedge +# Connect to browser via extension +playwright-cli open --extension + +# Use persistent profile (by default profile is in-memory) +playwright-cli open --persistent +# Use persistent profile with custom directory +playwright-cli open --profile=/path/to/profile + +# Start with config file +playwright-cli open --config=my-config.json + +# Close the browser +playwright-cli close +# Delete user data for the default session +playwright-cli delete-data +``` + +## Snapshots + +After each command, playwright-cli provides a snapshot of the current browser state. + +```bash +> playwright-cli goto https://example.com +### Page +- Page URL: https://example.com/ +- Page Title: Example Domain +### Snapshot +[Snapshot](.playwright-cli/page-2026-02-14T19-22-42-679Z.yml) +``` + +You can also take a snapshot on demand using `playwright-cli snapshot` command. + +If `--filename` is not provided, a new snapshot file is created with a timestamp. Default to automatic file naming, use `--filename=` when artifact is a part of the workflow result. + +## Browser Sessions + +```bash +# create new browser session named "mysession" with persistent profile +playwright-cli -s=mysession open example.com --persistent +# same with manually specified profile directory (use when requested explicitly) +playwright-cli -s=mysession open example.com --profile=/path/to/profile +playwright-cli -s=mysession click e6 +playwright-cli -s=mysession close # stop a named browser +playwright-cli -s=mysession delete-data # delete user data for persistent session + +playwright-cli list +# Close all browsers +playwright-cli close-all +# Forcefully kill all browser processes +playwright-cli kill-all +``` + +## Local installation + +In some cases user might want to install playwright-cli locally. If running globally available `playwright-cli` binary fails, use `npx playwright-cli` to run the commands. For example: + +```bash +npx playwright-cli open https://example.com +npx playwright-cli click e1 +``` + +## Example: Form submission + +```bash +playwright-cli open https://example.com/form +playwright-cli snapshot + +playwright-cli fill e1 "user@example.com" +playwright-cli fill e2 "password123" +playwright-cli click e3 +playwright-cli snapshot +playwright-cli close +``` + +## Example: Multi-tab workflow + +```bash +playwright-cli open https://example.com +playwright-cli tab-new https://example.com/other +playwright-cli tab-list +playwright-cli tab-select 0 +playwright-cli snapshot +playwright-cli close +``` + +## Example: Debugging with DevTools + +```bash +playwright-cli open https://example.com +playwright-cli click e4 +playwright-cli fill e7 "test" +playwright-cli console +playwright-cli network +playwright-cli close +``` + +```bash +playwright-cli open https://example.com +playwright-cli tracing-start +playwright-cli click e4 +playwright-cli fill e7 "test" +playwright-cli tracing-stop +playwright-cli close +``` + +## Specific tasks + +* **Request mocking** [references/request-mocking.md](references/request-mocking.md) +* **Running Playwright code** [references/running-code.md](references/running-code.md) +* **Browser session management** [references/session-management.md](references/session-management.md) +* **Storage state (cookies, localStorage)** [references/storage-state.md](references/storage-state.md) +* **Test generation** [references/test-generation.md](references/test-generation.md) +* **Tracing** [references/tracing.md](references/tracing.md) +* **Video recording** [references/video-recording.md](references/video-recording.md) diff --git a/.claude/skills/playwright-cli/references/request-mocking.md b/.claude/skills/playwright-cli/references/request-mocking.md new file mode 100644 index 0000000..9005fda --- /dev/null +++ b/.claude/skills/playwright-cli/references/request-mocking.md @@ -0,0 +1,87 @@ +# Request Mocking + +Intercept, mock, modify, and block network requests. + +## CLI Route Commands + +```bash +# Mock with custom status +playwright-cli route "**/*.jpg" --status=404 + +# Mock with JSON body +playwright-cli route "**/api/users" --body='[{"id":1,"name":"Alice"}]' --content-type=application/json + +# Mock with custom headers +playwright-cli route "**/api/data" --body='{"ok":true}' --header="X-Custom: value" + +# Remove headers from requests +playwright-cli route "**/*" --remove-header=cookie,authorization + +# List active routes +playwright-cli route-list + +# Remove a route or all routes +playwright-cli unroute "**/*.jpg" +playwright-cli unroute +``` + +## URL Patterns + +``` +**/api/users - Exact path match +**/api/*/details - Wildcard in path +**/*.{png,jpg,jpeg} - Match file extensions +**/search?q=* - Match query parameters +``` + +## Advanced Mocking with run-code + +For conditional responses, request body inspection, response modification, or delays: + +### Conditional Response Based on Request + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/login', route => { + const body = route.request().postDataJSON(); + if (body.username === 'admin') { + route.fulfill({ body: JSON.stringify({ token: 'mock-token' }) }); + } else { + route.fulfill({ status: 401, body: JSON.stringify({ error: 'Invalid' }) }); + } + }); +}" +``` + +### Modify Real Response + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/user', async route => { + const response = await route.fetch(); + const json = await response.json(); + json.isPremium = true; + await route.fulfill({ response, json }); + }); +}" +``` + +### Simulate Network Failures + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/offline', route => route.abort('internetdisconnected')); +}" +# Options: connectionrefused, timedout, connectionreset, internetdisconnected +``` + +### Delayed Response + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/slow', async route => { + await new Promise(r => setTimeout(r, 3000)); + route.fulfill({ body: JSON.stringify({ data: 'loaded' }) }); + }); +}" +``` diff --git a/.claude/skills/playwright-cli/references/running-code.md b/.claude/skills/playwright-cli/references/running-code.md new file mode 100644 index 0000000..7d6d22f --- /dev/null +++ b/.claude/skills/playwright-cli/references/running-code.md @@ -0,0 +1,232 @@ +# Running Custom Playwright Code + +Use `run-code` to execute arbitrary Playwright code for advanced scenarios not covered by CLI commands. + +## Syntax + +```bash +playwright-cli run-code "async page => { + // Your Playwright code here + // Access page.context() for browser context operations +}" +``` + +## Geolocation + +```bash +# Grant geolocation permission and set location +playwright-cli run-code "async page => { + await page.context().grantPermissions(['geolocation']); + await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 }); +}" + +# Set location to London +playwright-cli run-code "async page => { + await page.context().grantPermissions(['geolocation']); + await page.context().setGeolocation({ latitude: 51.5074, longitude: -0.1278 }); +}" + +# Clear geolocation override +playwright-cli run-code "async page => { + await page.context().clearPermissions(); +}" +``` + +## Permissions + +```bash +# Grant multiple permissions +playwright-cli run-code "async page => { + await page.context().grantPermissions([ + 'geolocation', + 'notifications', + 'camera', + 'microphone' + ]); +}" + +# Grant permissions for specific origin +playwright-cli run-code "async page => { + await page.context().grantPermissions(['clipboard-read'], { + origin: 'https://example.com' + }); +}" +``` + +## Media Emulation + +```bash +# Emulate dark color scheme +playwright-cli run-code "async page => { + await page.emulateMedia({ colorScheme: 'dark' }); +}" + +# Emulate light color scheme +playwright-cli run-code "async page => { + await page.emulateMedia({ colorScheme: 'light' }); +}" + +# Emulate reduced motion +playwright-cli run-code "async page => { + await page.emulateMedia({ reducedMotion: 'reduce' }); +}" + +# Emulate print media +playwright-cli run-code "async page => { + await page.emulateMedia({ media: 'print' }); +}" +``` + +## Wait Strategies + +```bash +# Wait for network idle +playwright-cli run-code "async page => { + await page.waitForLoadState('networkidle'); +}" + +# Wait for specific element +playwright-cli run-code "async page => { + await page.waitForSelector('.loading', { state: 'hidden' }); +}" + +# Wait for function to return true +playwright-cli run-code "async page => { + await page.waitForFunction(() => window.appReady === true); +}" + +# Wait with timeout +playwright-cli run-code "async page => { + await page.waitForSelector('.result', { timeout: 10000 }); +}" +``` + +## Frames and Iframes + +```bash +# Work with iframe +playwright-cli run-code "async page => { + const frame = page.locator('iframe#my-iframe').contentFrame(); + await frame.locator('button').click(); +}" + +# Get all frames +playwright-cli run-code "async page => { + const frames = page.frames(); + return frames.map(f => f.url()); +}" +``` + +## File Downloads + +```bash +# Handle file download +playwright-cli run-code "async page => { + const [download] = await Promise.all([ + page.waitForEvent('download'), + page.click('a.download-link') + ]); + await download.saveAs('./downloaded-file.pdf'); + return download.suggestedFilename(); +}" +``` + +## Clipboard + +```bash +# Read clipboard (requires permission) +playwright-cli run-code "async page => { + await page.context().grantPermissions(['clipboard-read']); + return await page.evaluate(() => navigator.clipboard.readText()); +}" + +# Write to clipboard +playwright-cli run-code "async page => { + await page.evaluate(text => navigator.clipboard.writeText(text), 'Hello clipboard!'); +}" +``` + +## Page Information + +```bash +# Get page title +playwright-cli run-code "async page => { + return await page.title(); +}" + +# Get current URL +playwright-cli run-code "async page => { + return page.url(); +}" + +# Get page content +playwright-cli run-code "async page => { + return await page.content(); +}" + +# Get viewport size +playwright-cli run-code "async page => { + return page.viewportSize(); +}" +``` + +## JavaScript Execution + +```bash +# Execute JavaScript and return result +playwright-cli run-code "async page => { + return await page.evaluate(() => { + return { + userAgent: navigator.userAgent, + language: navigator.language, + cookiesEnabled: navigator.cookieEnabled + }; + }); +}" + +# Pass arguments to evaluate +playwright-cli run-code "async page => { + const multiplier = 5; + return await page.evaluate(m => document.querySelectorAll('li').length * m, multiplier); +}" +``` + +## Error Handling + +```bash +# Try-catch in run-code +playwright-cli run-code "async page => { + try { + await page.click('.maybe-missing', { timeout: 1000 }); + return 'clicked'; + } catch (e) { + return 'element not found'; + } +}" +``` + +## Complex Workflows + +```bash +# Login and save state +playwright-cli run-code "async page => { + await page.goto('https://example.com/login'); + await page.fill('input[name=email]', 'user@example.com'); + await page.fill('input[name=password]', 'secret'); + await page.click('button[type=submit]'); + await page.waitForURL('**/dashboard'); + await page.context().storageState({ path: 'auth.json' }); + return 'Login successful'; +}" + +# Scrape data from multiple pages +playwright-cli run-code "async page => { + const results = []; + for (let i = 1; i <= 3; i++) { + await page.goto(\`https://example.com/page/\${i}\`); + const items = await page.locator('.item').allTextContents(); + results.push(...items); + } + return results; +}" +``` diff --git a/.claude/skills/playwright-cli/references/session-management.md b/.claude/skills/playwright-cli/references/session-management.md new file mode 100644 index 0000000..fac9606 --- /dev/null +++ b/.claude/skills/playwright-cli/references/session-management.md @@ -0,0 +1,169 @@ +# Browser Session Management + +Run multiple isolated browser sessions concurrently with state persistence. + +## Named Browser Sessions + +Use `-s` flag to isolate browser contexts: + +```bash +# Browser 1: Authentication flow +playwright-cli -s=auth open https://app.example.com/login + +# Browser 2: Public browsing (separate cookies, storage) +playwright-cli -s=public open https://example.com + +# Commands are isolated by browser session +playwright-cli -s=auth fill e1 "user@example.com" +playwright-cli -s=public snapshot +``` + +## Browser Session Isolation Properties + +Each browser session has independent: +- Cookies +- LocalStorage / SessionStorage +- IndexedDB +- Cache +- Browsing history +- Open tabs + +## Browser Session Commands + +```bash +# List all browser sessions +playwright-cli list + +# Stop a browser session (close the browser) +playwright-cli close # stop the default browser +playwright-cli -s=mysession close # stop a named browser + +# Stop all browser sessions +playwright-cli close-all + +# Forcefully kill all daemon processes (for stale/zombie processes) +playwright-cli kill-all + +# Delete browser session user data (profile directory) +playwright-cli delete-data # delete default browser data +playwright-cli -s=mysession delete-data # delete named browser data +``` + +## Environment Variable + +Set a default browser session name via environment variable: + +```bash +export PLAYWRIGHT_CLI_SESSION="mysession" +playwright-cli open example.com # Uses "mysession" automatically +``` + +## Common Patterns + +### Concurrent Scraping + +```bash +#!/bin/bash +# Scrape multiple sites concurrently + +# Start all browsers +playwright-cli -s=site1 open https://site1.com & +playwright-cli -s=site2 open https://site2.com & +playwright-cli -s=site3 open https://site3.com & +wait + +# Take snapshots from each +playwright-cli -s=site1 snapshot +playwright-cli -s=site2 snapshot +playwright-cli -s=site3 snapshot + +# Cleanup +playwright-cli close-all +``` + +### A/B Testing Sessions + +```bash +# Test different user experiences +playwright-cli -s=variant-a open "https://app.com?variant=a" +playwright-cli -s=variant-b open "https://app.com?variant=b" + +# Compare +playwright-cli -s=variant-a screenshot +playwright-cli -s=variant-b screenshot +``` + +### Persistent Profile + +By default, browser profile is kept in memory only. Use `--persistent` flag on `open` to persist the browser profile to disk: + +```bash +# Use persistent profile (auto-generated location) +playwright-cli open https://example.com --persistent + +# Use persistent profile with custom directory +playwright-cli open https://example.com --profile=/path/to/profile +``` + +## Default Browser Session + +When `-s` is omitted, commands use the default browser session: + +```bash +# These use the same default browser session +playwright-cli open https://example.com +playwright-cli snapshot +playwright-cli close # Stops default browser +``` + +## Browser Session Configuration + +Configure a browser session with specific settings when opening: + +```bash +# Open with config file +playwright-cli open https://example.com --config=.playwright/my-cli.json + +# Open with specific browser +playwright-cli open https://example.com --browser=firefox + +# Open in headed mode +playwright-cli open https://example.com --headed + +# Open with persistent profile +playwright-cli open https://example.com --persistent +``` + +## Best Practices + +### 1. Name Browser Sessions Semantically + +```bash +# GOOD: Clear purpose +playwright-cli -s=github-auth open https://github.com +playwright-cli -s=docs-scrape open https://docs.example.com + +# AVOID: Generic names +playwright-cli -s=s1 open https://github.com +``` + +### 2. Always Clean Up + +```bash +# Stop browsers when done +playwright-cli -s=auth close +playwright-cli -s=scrape close + +# Or stop all at once +playwright-cli close-all + +# If browsers become unresponsive or zombie processes remain +playwright-cli kill-all +``` + +### 3. Delete Stale Browser Data + +```bash +# Remove old browser data to free disk space +playwright-cli -s=oldsession delete-data +``` diff --git a/.claude/skills/playwright-cli/references/storage-state.md b/.claude/skills/playwright-cli/references/storage-state.md new file mode 100644 index 0000000..c856db5 --- /dev/null +++ b/.claude/skills/playwright-cli/references/storage-state.md @@ -0,0 +1,275 @@ +# Storage Management + +Manage cookies, localStorage, sessionStorage, and browser storage state. + +## Storage State + +Save and restore complete browser state including cookies and storage. + +### Save Storage State + +```bash +# Save to auto-generated filename (storage-state-{timestamp}.json) +playwright-cli state-save + +# Save to specific filename +playwright-cli state-save my-auth-state.json +``` + +### Restore Storage State + +```bash +# Load storage state from file +playwright-cli state-load my-auth-state.json + +# Reload page to apply cookies +playwright-cli open https://example.com +``` + +### Storage State File Format + +The saved file contains: + +```json +{ + "cookies": [ + { + "name": "session_id", + "value": "abc123", + "domain": "example.com", + "path": "/", + "expires": 1735689600, + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + } + ], + "origins": [ + { + "origin": "https://example.com", + "localStorage": [ + { "name": "theme", "value": "dark" }, + { "name": "user_id", "value": "12345" } + ] + } + ] +} +``` + +## Cookies + +### List All Cookies + +```bash +playwright-cli cookie-list +``` + +### Filter Cookies by Domain + +```bash +playwright-cli cookie-list --domain=example.com +``` + +### Filter Cookies by Path + +```bash +playwright-cli cookie-list --path=/api +``` + +### Get Specific Cookie + +```bash +playwright-cli cookie-get session_id +``` + +### Set a Cookie + +```bash +# Basic cookie +playwright-cli cookie-set session abc123 + +# Cookie with options +playwright-cli cookie-set session abc123 --domain=example.com --path=/ --httpOnly --secure --sameSite=Lax + +# Cookie with expiration (Unix timestamp) +playwright-cli cookie-set remember_me token123 --expires=1735689600 +``` + +### Delete a Cookie + +```bash +playwright-cli cookie-delete session_id +``` + +### Clear All Cookies + +```bash +playwright-cli cookie-clear +``` + +### Advanced: Multiple Cookies or Custom Options + +For complex scenarios like adding multiple cookies at once, use `run-code`: + +```bash +playwright-cli run-code "async page => { + await page.context().addCookies([ + { name: 'session_id', value: 'sess_abc123', domain: 'example.com', path: '/', httpOnly: true }, + { name: 'preferences', value: JSON.stringify({ theme: 'dark' }), domain: 'example.com', path: '/' } + ]); +}" +``` + +## Local Storage + +### List All localStorage Items + +```bash +playwright-cli localstorage-list +``` + +### Get Single Value + +```bash +playwright-cli localstorage-get token +``` + +### Set Value + +```bash +playwright-cli localstorage-set theme dark +``` + +### Set JSON Value + +```bash +playwright-cli localstorage-set user_settings '{"theme":"dark","language":"en"}' +``` + +### Delete Single Item + +```bash +playwright-cli localstorage-delete token +``` + +### Clear All localStorage + +```bash +playwright-cli localstorage-clear +``` + +### Advanced: Multiple Operations + +For complex scenarios like setting multiple values at once, use `run-code`: + +```bash +playwright-cli run-code "async page => { + await page.evaluate(() => { + localStorage.setItem('token', 'jwt_abc123'); + localStorage.setItem('user_id', '12345'); + localStorage.setItem('expires_at', Date.now() + 3600000); + }); +}" +``` + +## Session Storage + +### List All sessionStorage Items + +```bash +playwright-cli sessionstorage-list +``` + +### Get Single Value + +```bash +playwright-cli sessionstorage-get form_data +``` + +### Set Value + +```bash +playwright-cli sessionstorage-set step 3 +``` + +### Delete Single Item + +```bash +playwright-cli sessionstorage-delete step +``` + +### Clear sessionStorage + +```bash +playwright-cli sessionstorage-clear +``` + +## IndexedDB + +### List Databases + +```bash +playwright-cli run-code "async page => { + return await page.evaluate(async () => { + const databases = await indexedDB.databases(); + return databases; + }); +}" +``` + +### Delete Database + +```bash +playwright-cli run-code "async page => { + await page.evaluate(() => { + indexedDB.deleteDatabase('myDatabase'); + }); +}" +``` + +## Common Patterns + +### Authentication State Reuse + +```bash +# Step 1: Login and save state +playwright-cli open https://app.example.com/login +playwright-cli snapshot +playwright-cli fill e1 "user@example.com" +playwright-cli fill e2 "password123" +playwright-cli click e3 + +# Save the authenticated state +playwright-cli state-save auth.json + +# Step 2: Later, restore state and skip login +playwright-cli state-load auth.json +playwright-cli open https://app.example.com/dashboard +# Already logged in! +``` + +### Save and Restore Roundtrip + +```bash +# Set up authentication state +playwright-cli open https://example.com +playwright-cli eval "() => { document.cookie = 'session=abc123'; localStorage.setItem('user', 'john'); }" + +# Save state to file +playwright-cli state-save my-session.json + +# ... later, in a new session ... + +# Restore state +playwright-cli state-load my-session.json +playwright-cli open https://example.com +# Cookies and localStorage are restored! +``` + +## Security Notes + +- Never commit storage state files containing auth tokens +- Add `*.auth-state.json` to `.gitignore` +- Delete state files after automation completes +- Use environment variables for sensitive data +- By default, sessions run in-memory mode which is safer for sensitive operations diff --git a/.claude/skills/playwright-cli/references/test-generation.md b/.claude/skills/playwright-cli/references/test-generation.md new file mode 100644 index 0000000..7a09df3 --- /dev/null +++ b/.claude/skills/playwright-cli/references/test-generation.md @@ -0,0 +1,88 @@ +# Test Generation + +Generate Playwright test code automatically as you interact with the browser. + +## How It Works + +Every action you perform with `playwright-cli` generates corresponding Playwright TypeScript code. +This code appears in the output and can be copied directly into your test files. + +## Example Workflow + +```bash +# Start a session +playwright-cli open https://example.com/login + +# Take a snapshot to see elements +playwright-cli snapshot +# Output shows: e1 [textbox "Email"], e2 [textbox "Password"], e3 [button "Sign In"] + +# Fill form fields - generates code automatically +playwright-cli fill e1 "user@example.com" +# Ran Playwright code: +# await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com'); + +playwright-cli fill e2 "password123" +# Ran Playwright code: +# await page.getByRole('textbox', { name: 'Password' }).fill('password123'); + +playwright-cli click e3 +# Ran Playwright code: +# await page.getByRole('button', { name: 'Sign In' }).click(); +``` + +## Building a Test File + +Collect the generated code into a Playwright test: + +```typescript +import { test, expect } from '@playwright/test'; + +test('login flow', async ({ page }) => { + // Generated code from playwright-cli session: + await page.goto('https://example.com/login'); + await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com'); + await page.getByRole('textbox', { name: 'Password' }).fill('password123'); + await page.getByRole('button', { name: 'Sign In' }).click(); + + // Add assertions + await expect(page).toHaveURL(/.*dashboard/); +}); +``` + +## Best Practices + +### 1. Use Semantic Locators + +The generated code uses role-based locators when possible, which are more resilient: + +```typescript +// Generated (good - semantic) +await page.getByRole('button', { name: 'Submit' }).click(); + +// Avoid (fragile - CSS selectors) +await page.locator('#submit-btn').click(); +``` + +### 2. Explore Before Recording + +Take snapshots to understand the page structure before recording actions: + +```bash +playwright-cli open https://example.com +playwright-cli snapshot +# Review the element structure +playwright-cli click e5 +``` + +### 3. Add Assertions Manually + +Generated code captures actions but not assertions. Add expectations in your test: + +```typescript +// Generated action +await page.getByRole('button', { name: 'Submit' }).click(); + +// Manual assertion +await expect(page.getByText('Success')).toBeVisible(); +``` diff --git a/.claude/skills/playwright-cli/references/tracing.md b/.claude/skills/playwright-cli/references/tracing.md new file mode 100644 index 0000000..7ce7bab --- /dev/null +++ b/.claude/skills/playwright-cli/references/tracing.md @@ -0,0 +1,139 @@ +# Tracing + +Capture detailed execution traces for debugging and analysis. Traces include DOM snapshots, screenshots, network activity, and console logs. + +## Basic Usage + +```bash +# Start trace recording +playwright-cli tracing-start + +# Perform actions +playwright-cli open https://example.com +playwright-cli click e1 +playwright-cli fill e2 "test" + +# Stop trace recording +playwright-cli tracing-stop +``` + +## Trace Output Files + +When you start tracing, Playwright creates a `traces/` directory with several files: + +### `trace-{timestamp}.trace` + +**Action log** - The main trace file containing: +- Every action performed (clicks, fills, navigations) +- DOM snapshots before and after each action +- Screenshots at each step +- Timing information +- Console messages +- Source locations + +### `trace-{timestamp}.network` + +**Network log** - Complete network activity: +- All HTTP requests and responses +- Request headers and bodies +- Response headers and bodies +- Timing (DNS, connect, TLS, TTFB, download) +- Resource sizes +- Failed requests and errors + +### `resources/` + +**Resources directory** - Cached resources: +- Images, fonts, stylesheets, scripts +- Response bodies for replay +- Assets needed to reconstruct page state + +## What Traces Capture + +| Category | Details | +|----------|---------| +| **Actions** | Clicks, fills, hovers, keyboard input, navigations | +| **DOM** | Full DOM snapshot before/after each action | +| **Screenshots** | Visual state at each step | +| **Network** | All requests, responses, headers, bodies, timing | +| **Console** | All console.log, warn, error messages | +| **Timing** | Precise timing for each operation | + +## Use Cases + +### Debugging Failed Actions + +```bash +playwright-cli tracing-start +playwright-cli open https://app.example.com + +# This click fails - why? +playwright-cli click e5 + +playwright-cli tracing-stop +# Open trace to see DOM state when click was attempted +``` + +### Analyzing Performance + +```bash +playwright-cli tracing-start +playwright-cli open https://slow-site.com +playwright-cli tracing-stop + +# View network waterfall to identify slow resources +``` + +### Capturing Evidence + +```bash +# Record a complete user flow for documentation +playwright-cli tracing-start + +playwright-cli open https://app.example.com/checkout +playwright-cli fill e1 "4111111111111111" +playwright-cli fill e2 "12/25" +playwright-cli fill e3 "123" +playwright-cli click e4 + +playwright-cli tracing-stop +# Trace shows exact sequence of events +``` + +## Trace vs Video vs Screenshot + +| Feature | Trace | Video | Screenshot | +|---------|-------|-------|------------| +| **Format** | .trace file | .webm video | .png/.jpeg image | +| **DOM inspection** | Yes | No | No | +| **Network details** | Yes | No | No | +| **Step-by-step replay** | Yes | Continuous | Single frame | +| **File size** | Medium | Large | Small | +| **Best for** | Debugging | Demos | Quick capture | + +## Best Practices + +### 1. Start Tracing Before the Problem + +```bash +# Trace the entire flow, not just the failing step +playwright-cli tracing-start +playwright-cli open https://example.com +# ... all steps leading to the issue ... +playwright-cli tracing-stop +``` + +### 2. Clean Up Old Traces + +Traces can consume significant disk space: + +```bash +# Remove traces older than 7 days +find .playwright-cli/traces -mtime +7 -delete +``` + +## Limitations + +- Traces add overhead to automation +- Large traces can consume significant disk space +- Some dynamic content may not replay perfectly diff --git a/.claude/skills/playwright-cli/references/video-recording.md b/.claude/skills/playwright-cli/references/video-recording.md new file mode 100644 index 0000000..38391b3 --- /dev/null +++ b/.claude/skills/playwright-cli/references/video-recording.md @@ -0,0 +1,43 @@ +# Video Recording + +Capture browser automation sessions as video for debugging, documentation, or verification. Produces WebM (VP8/VP9 codec). + +## Basic Recording + +```bash +# Start recording +playwright-cli video-start + +# Perform actions +playwright-cli open https://example.com +playwright-cli snapshot +playwright-cli click e1 +playwright-cli fill e2 "test input" + +# Stop and save +playwright-cli video-stop demo.webm +``` + +## Best Practices + +### 1. Use Descriptive Filenames + +```bash +# Include context in filename +playwright-cli video-stop recordings/login-flow-2024-01-15.webm +playwright-cli video-stop recordings/checkout-test-run-42.webm +``` + +## Tracing vs Video + +| Feature | Video | Tracing | +|---------|-------|---------| +| Output | WebM file | Trace file (viewable in Trace Viewer) | +| Shows | Visual recording | DOM snapshots, network, console, actions | +| Use case | Demos, documentation | Debugging, analysis | +| Size | Larger | Smaller | + +## Limitations + +- Recording adds slight overhead to automation +- Large recordings can consume significant disk space diff --git a/.claude/skills/playwright.md b/.claude/skills/playwright.md new file mode 100644 index 0000000..ca9ed9c --- /dev/null +++ b/.claude/skills/playwright.md @@ -0,0 +1,143 @@ +# Playwright Skills – DynamoHome + +## Configuration + +- **Config file**: `playwright.config.js` (`testDir: './tests/e2e'`) +- **Browser**: Chromium only (Desktop Chrome profile) +- **Base URL**: `http://localhost:8080` (dev server must be running) +- **Timeouts**: 30s per test, 5s for expect assertions +- **Retries**: 2 on CI, 0 locally +- **Run tests**: `npm run test:e2e` (targets `tests/e2e/e2e.test.ts`) +- **Prerequisite**: `npm run start` must be running (or configure the `webServer` in config which auto-starts it) + +## Required folder structure + +``` +tests/ + e2e/ + e2e.test.ts # Test orchestration only — NO selectors, NO page.locator() + pages/ + RecentPage.ts # Page class for Recent files page + SamplesPage.ts # Page class for Samples page + LearningPage.ts # Page class for Learning page + components/ + Sidebar.ts # Sidebar navigation component class + GraphTable.ts # Table component class (used by Recent + Samples) + Carousel.ts # Carousel component class (used by Learning) + CardItem.ts # Grid card component class + unit/ + App.test.tsx # Unit tests — separate from e2e + jest.setup.ts # Jest setup (not used by Playwright) + __mocks__/ # Jest mocks (not used by Playwright) +``` + +## Page Object Model — mandatory pattern + +### Page class + +```typescript +// tests/e2e/pages/RecentPage.ts +import { Page, Locator } from '@playwright/test'; + +export class RecentPage { + private readonly gridViewToggle: Locator; + private readonly listViewToggle: Locator; + private readonly graphCards: Locator; + + constructor(private page: Page) { + this.gridViewToggle = page.locator('[data-testid="grid-view-toggle"]'); + this.listViewToggle = page.locator('[data-testid="list-view-toggle"]'); + this.graphCards = page.locator('[data-testid="graph-card"]'); + } + + async switchToGridView(): Promise { + await this.gridViewToggle.click(); + } + + async switchToListView(): Promise { + await this.listViewToggle.click(); + } + + async getGraphCount(): Promise { + return this.graphCards.count(); + } + + async openGraphByName(name: string): Promise { + await this.graphCards.filter({ hasText: name }).dblclick(); + } +} +``` + +### Test file + +```typescript +// tests/e2e/e2e.test.ts +import { test, expect } from '@playwright/test'; +import { RecentPage } from './pages/RecentPage'; +import { Sidebar } from './components/Sidebar'; + +test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:8080'); +}); + +test('recent page shows graphs in grid view by default', async ({ page }) => { + const sidebar = new Sidebar(page); + const recentPage = new RecentPage(page); + + await sidebar.navigateToRecent(); + const count = await recentPage.getGraphCount(); + expect(count).toBeGreaterThan(0); +}); + +test('recent page switches to list view', async ({ page }) => { + const recentPage = new RecentPage(page); + await recentPage.switchToListView(); + // assert table is visible via page object method + const isTableVisible = await recentPage.isTableVisible(); + expect(isTableVisible).toBe(true); +}); +``` + +## Hard rules + +| Rule | Example violation | +|---|---| +| No selectors in test files | `await page.locator('.graph-card').click()` in `e2e.test.ts` | +| No direct page actions in test files | `await page.click('[data-testid="button"]')` in `e2e.test.ts` | +| No `waitForTimeout()` | `await page.waitForTimeout(2000)` | +| All selectors defined in Page/Component constructors | Inline `page.locator()` in action methods | + +## Selector strategy + +Prefer in this order: +1. `data-testid` attributes (`[data-testid="graph-card"]`) — most stable +2. ARIA roles (`page.getByRole('button', { name: 'Open' })`) +3. Text content (`page.getByText('Recent Files')`) — for labels only +4. CSS selectors — last resort, avoid + +If a `data-testid` is missing from a component, add it via the frontend-agent before writing the test. + +## Waiting strategy + +```typescript +// ✅ Wait for element to be visible +await page.waitForSelector('[data-testid="graph-card"]'); + +// ✅ Use expect-based waiting (auto-retries) +await expect(recentPage.getGraphCards()).toHaveCount(5); + +// ❌ Never arbitrary sleep +await page.waitForTimeout(2000); +``` + +## Exploratory testing + +```bash +# Open the app for exploratory testing and selector discovery +playwright-cli open http://localhost:8080 + +# View HTML test report after a run +npx playwright show-report +``` + +Convert findings from exploration into formal Page Object classes — never leave raw `page.locator()` calls in test files. diff --git a/.claude/skills/react.md b/.claude/skills/react.md new file mode 100644 index 0000000..407c2ed --- /dev/null +++ b/.claude/skills/react.md @@ -0,0 +1,77 @@ +# React Skills – DynamoHome + +## Component rules + +**Always use functional components:** +```tsx +// ✅ +export const MyComponent = ({ title }: MyComponentProps) =>
{title}
; + +// ❌ Never +export class MyComponent extends React.Component { ... } +``` + +**Define explicit prop interfaces:** +```tsx +// ✅ +interface GraphGridItemProps { + name: string; + path: string; + onOpen: (path: string) => void; +} +export const GraphGridItem = ({ name, path, onOpen }: GraphGridItemProps) => { ... }; + +// ❌ +export const GraphGridItem = ({ name, path, onOpen }: any) => { ... }; +``` + +**CSS Modules — mandatory:** +```tsx +import styles from './GraphGridItem.module.css'; +// Access with bracket notation +
+// ❌ Never global class names or inline styles +
+
+``` + +## State management + +- Component-level state: `useState` +- Shared user preferences: `useSettings()` hook (wraps `SettingsContext`) +- No external state libraries — Context API only + +```tsx +import { useSettings } from '../SettingsContext'; +const { recentPageViewMode, setRecentPageViewMode } = useSettings(); +``` + +## Hooks + +- Use built-in hooks: `useState`, `useEffect`, `useCallback`, `useMemo`, `useRef`, `useContext` +- Consume context via `useSettings()` — do not use `useContext(SettingsContext)` directly +- Use `useIntl()` from react-intl when you need a string value (not JSX) + +## Approved libraries — do not add others + +| Library | Usage | +|---|---| +| react-intl | ``, `useIntl()` | +| react-table | `useTable()` hook in GraphTable and SamplesTable | +| react-split-pane | `` in LayoutContainer | + +## Folder placement + +| Component type | Location | +|---|---| +| Used by one module | `src/components/[Module]/` | +| Used by 2+ modules | `src/components/Common/` | +| App-level | `src/components/` (LayoutContainer, MainContent, SettingsContext) | + +## What NOT to do + +- Do not add Redux, Zustand, Recoil, MobX, Jotai, or any state management library +- Do not add component libraries (Material UI, Ant Design, Chakra, shadcn, etc.) +- Do not use `React.FC` type — prefer explicit prop interfaces with arrow function components +- Do not use `React.memo`, `React.lazy`, or `Suspense` unless explicitly requested +- Do not use `any` for prop types, state, or return types in new code diff --git a/.claude/skills/unit-testing.md b/.claude/skills/unit-testing.md new file mode 100644 index 0000000..1a8ec78 --- /dev/null +++ b/.claude/skills/unit-testing.md @@ -0,0 +1,101 @@ +# Unit Testing Skills – DynamoHome + +## Stack + +- **Runner**: Jest 29 with `ts-jest` preset +- **DOM**: `jest-environment-jsdom` +- **Component testing**: `@testing-library/react` 15 +- **Run tests**: `npm run test:unit` (targets `tests/unit/`) +- **Coverage output**: `./coverage/` (lcov + text reporters) +- **Coverage target**: 100% for all files under `src/` + +## Test file location + +``` +tests/ + unit/ + App.test.tsx # Existing app-level test + ComponentName.test.tsx # One test file per component — place new tests here + jest.setup.ts # Applied via setupFilesAfterEnv — do not modify + __mocks__/ + chromeMock.ts # Auto-applied: mocks window.chrome.webview globals + fileMock.ts # Auto-applied: mocks image imports + styleMock.ts # Auto-applied: mocks CSS module imports +``` + +## Mocks — what's already set up + +The `chromeMock.ts` is applied globally in `jest.setup.ts`. It mocks: +- `window.chrome.webview.hostObjects.scriptObject` (all backend methods) +- Global callbacks like `window.receiveGraphDataFromDotNet` + +**Do not re-mock these in individual test files** — they're already available. + +CSS Modules are mocked by `identity-obj-proxy` — `styles['class-name']` returns `'class-name'` in tests. + +## Writing a test + +```tsx +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import messages from '../../src/locales/en.json'; +import { CardItem } from '../../src/components/Common/CardItem'; + +// Wrap with IntlProvider for components using FormattedMessage +const renderWithIntl = (ui: React.ReactElement) => + render( + + {ui} + + ); + +describe('CardItem', () => { + it('renders the title', () => { + renderWithIntl(); + expect(screen.getByText('My Graph')).toBeInTheDocument(); + }); + + it('calls onClick when clicked', () => { + const handleClick = jest.fn(); + renderWithIntl(); + fireEvent.click(screen.getByRole('button')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); +}); +``` + +## What to test + +- **Render output**: does the component render expected text, elements, attributes? +- **User interactions**: click, hover, keyboard events — use `fireEvent` or `userEvent` +- **Conditional rendering**: does it show/hide elements based on props or state? +- **Context consumption**: does it read from SettingsContext correctly? + +## What NOT to test + +- Internal implementation details (state variable names, private methods) +- CSS class names (CSS modules are mocked) +- Third-party library internals (react-table, react-split-pane) +- The Dynamo backend (mocked globally via chromeMock) + +## Context testing + +For components that use `useSettings()`: +```tsx +import { SettingsContext } from '../../src/components/SettingsContext'; + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +render(, { wrapper }); +``` + +## Coverage requirement + +- Every component you **create or modify** must have a corresponding test file +- Run `npm run test:unit -- --coverage` to check coverage after writing tests (output in `./coverage/`) +- Do not submit work that reduces existing coverage diff --git a/.claude/workflows/bugfix.md b/.claude/workflows/bugfix.md new file mode 100644 index 0000000..4816587 --- /dev/null +++ b/.claude/workflows/bugfix.md @@ -0,0 +1,68 @@ +# Bug Fix Workflow + +Use this workflow when diagnosing and fixing a bug in DynamoHome. + +## Steps + +### 1. Reproduce the issue + +Before touching any code: +- Identify the affected module: Recent, Samples, Learning, Sidebar, or layout +- Check if the bug occurs in dev mode (`npm run start`) or only inside Dynamo (WebView context) +- For dev-mode reproduction: start the server and observe the behavior with mock data +- For WebView-only bugs: the root cause is likely in `src/functions/utility.ts` or the `window.chrome?.webview` guard logic + +### 2. Locate the root cause + +```bash +# Find components related to the bug +grep -r "keyword" src/components/ + +# Check how data flows to the affected component +# Trace from window globals → component setState → render +``` + +Common bug locations: +- **Data not displaying**: check `receiveXxxDataFromDotNet` callback and JSON parsing +- **Localization broken**: check locale key exists in all 14 locale files +- **Settings not persisting**: check `saveHomePageSettings()` in `utility.ts` and SettingsContext +- **Crash on load**: check `window.chrome?.webview` optional chaining guard +- **Wrong view mode**: check `SettingsContext` and the `recentPageViewMode`/`samplesViewMode` values + +### 3. Apply the fix + +- Make the **smallest change** that fixes the issue — do not refactor adjacent code +- Do not change unrelated files +- If the fix touches `window` globals or the settings JSON schema, check Dynamo compatibility: + - Global callback names must not change + - Settings field names must not change (adding new fields is safe) + +### 4. Add a regression test + +For every bug fixed, add or update a test that would have caught it: + +```tsx +// tests/unit/ComponentName.test.tsx +it('does not crash when graphData is empty', () => { + render(); + expect(screen.getByText(/no recent files/i)).toBeInTheDocument(); +}); +``` + +Run: `npm run test:unit` + +### 5. Verify everything passes + +```bash +npm run lint:check # No new lint errors +npm run test:unit # All unit tests pass (including new regression test) +npm run build # Dev bundle still builds +``` + +For bugs involving the full user flow, also run: `npm run test:e2e` + +## Dynamo-specific considerations + +- If the bug only occurs inside Dynamo (not in dev mode), read `src/functions/utility.ts` carefully — all WebView calls go through there +- The `hostObjects.scriptObject` methods are async — missing `await` is a common bug source +- JSON parsing errors from `GetHomePageSettings()` are caught with try-catch in `LayoutContainer.tsx` — check that logic if settings-related diff --git a/.claude/workflows/feature-ui.md b/.claude/workflows/feature-ui.md new file mode 100644 index 0000000..7b5fdc5 --- /dev/null +++ b/.claude/workflows/feature-ui.md @@ -0,0 +1,90 @@ +# UI Feature Development Workflow + +Use this workflow when implementing a new UI feature or modifying existing UI in DynamoHome. + +## Steps + +### 1. Identify scope + +Determine which module(s) are affected: +- **Recent** → `src/components/Recent/` +- **Samples** → `src/components/Samples/` +- **Learning** → `src/components/Learning/` +- **Sidebar** → `src/components/Sidebar/` +- **Shared** → `src/components/Common/` + +Read the existing components in that module before making changes. + +### 2. Add or update localized strings + +Before writing any JSX with user-visible text: + +a. Add the key to `src/locales/en.json` +b. Add the same key (English value as placeholder) to all 13 other locale files +c. Key format: `module.element.descriptor` (e.g., `recent.filter.label.search`) + +### 3. Implement the UI change + +Follow the React skills (`skills/react.md`): +- Functional component with explicit prop interface +- CSS Modules for styling +- `` or `useIntl()` for all text +- No new libraries + +If adding a new component: +- Place it in the correct module folder (or `Common/` if shared) +- Create `ComponentName.tsx` + `ComponentName.module.css` together + +### 4. Update SettingsContext if needed + +If the feature requires persisting user preferences (e.g., a new view mode): +- Add the new field to `SettingsContext.tsx` +- Update the settings JSON schema in `src/functions/utility.ts` +- Note: settings schema is shared with Dynamo — adding fields is safe, removing/renaming is a breaking change + +### 5. Write or update unit tests + +For every component created or modified, create/update `tests/unit/ComponentName.test.tsx`: +- Wrap with `IntlProvider` for components using `FormattedMessage` +- Test what renders, test user interactions +- Run: `npm run test:unit -- --coverage` +- Coverage must not decrease + +### 6. Verify build and lint + +```bash +npm run lint:check # Must pass with no errors +npm run build # Must produce a valid bundle +npm run test:unit # All tests must pass +``` + +### 7. Update E2E tests (if user flow changed) + +If the feature changes a user-visible flow (new page, new navigation, new interactive element): +- Add or update Page Object classes in `tests/e2e/pages/` or `tests/e2e/components/` +- Add test cases to `tests/e2e/e2e.test.ts` (orchestration only — no selectors in test file) +- Run: `npm run test:e2e` (requires `npm run start` running) + +## Quick reference — common patterns + +```tsx +// New component skeleton +interface MyFeatureProps { + value: string; + onChange: (v: string) => void; +} + +export const MyFeature = ({ value, onChange }: MyFeatureProps) => { + const intl = useIntl(); + return ( +
+ + onChange(e.target.value)} + placeholder={intl.formatMessage({ id: 'module.myfeature.placeholder' })} + /> +
+ ); +}; +``` diff --git a/.claude/workflows/pr-review.md b/.claude/workflows/pr-review.md new file mode 100644 index 0000000..d8ae694 --- /dev/null +++ b/.claude/workflows/pr-review.md @@ -0,0 +1,78 @@ +# PR Review Workflow + +Use this workflow when reviewing a pull request against the DynamoHome repository. + +## Steps + +### 1. Understand the scope + +Read the PR description and list of changed files. Identify: +- Which module(s) are touched: Recent, Samples, Learning, Sidebar, Common, build, tests +- Is it a feature, bugfix, refactor, or dependency update? +- Are there Dynamo integration touchpoints (window globals, settings schema, output path)? + +### 2. Review code changes + +Work through the `code-review` skill checklist (`skills/code-review.md`): + +**TypeScript**: no new `any`, no `@ts-ignore` + +**React**: functional components, explicit prop types, CSS Modules, no new libraries + +**Localization**: +- Every new user-visible string must be in `en.json` AND all 13 other locale files +- Keys follow `module.element.descriptor` format +- No hardcoded text in JSX + +**Dynamo integration**: +- `window.chrome?.webview` always optional-chained +- No rename/removal of global callbacks (`receiveGraphDataFromDotNet`, `setLocale`, etc.) +- No change to settings JSON field names or output bundle path + +### 3. Verify test coverage + +**Unit tests**: +- New/modified components have test files in `tests/unit/` +- Tests validate behavior, not implementation details +- Chrome globals not re-mocked (already in `tests/__mocks__/chromeMock.ts`) + +**E2E tests**: +- Changed user flows have Playwright test coverage +- `tests/e2e/e2e.test.ts` contains no selectors or direct page actions +- All selectors live in `tests/e2e/pages/` or `tests/e2e/components/` + +### 4. Check build integrity + +Confirm (or ask the author to confirm): +```bash +npm run lint:check # Passes +npm run test:unit # Passes +npm run build # Dev bundle succeeds +npm run bundle # Production bundle succeeds (for PRs touching webpack or build scripts) +``` + +### 5. Write feedback + +Structure feedback clearly: + +- **Blocker**: something that will break functionality, cause a regression, or violates a hard rule + > File and line number, what the problem is, what the fix should be +- **Recommendation**: optional improvement, clearly labeled as non-blocking + > Keep it brief; one issue per comment + +Do NOT: +- Request refactors for code not touched by the PR +- Flag style differences that match the surrounding file +- Block for pre-existing issues (legacy `any` types, missing tests for old components) +- Ask for architectural changes outside the PR scope + +## Common blockers to watch for + +| Issue | Why it blocks | +|---|---| +| Missing locale key in any of the 14 locale files | App throws at runtime for users of that locale | +| `window.chrome.webview` accessed without optional chaining | Crashes in dev mode / outside Dynamo | +| Global callback renamed or removed | Dynamo .NET host calls it by name — breaking change | +| Selector in `e2e.test.ts` directly | Violates POM requirement; makes tests brittle | +| New npm dependency without justification | Increases bundle size, adds supply chain risk | +| Output path changed from `dist/build/` | Dynamo integration breaks at runtime | diff --git a/.claude/workflows/refactor.md b/.claude/workflows/refactor.md new file mode 100644 index 0000000..dd9cc0a --- /dev/null +++ b/.claude/workflows/refactor.md @@ -0,0 +1,64 @@ +# Refactor Workflow + +Use this workflow when restructuring code without changing its behavior in DynamoHome. + +## Steps + +### 1. Define scope and goal + +Before touching code, clearly identify: +- What is being refactored and why (readability, duplication, type safety, etc.) +- What the observable behavior is (so you can verify it's preserved) +- What files are in scope — do not touch files outside the stated scope + +### 2. Establish a baseline + +Run the full test suite to confirm starting state is green: + +```bash +npm run lint:check +npm run test:unit +npm run build +``` + +If any of these fail before you start, stop and fix them first or confirm with the user. + +### 3. Refactor incrementally + +- Make one logical change at a time (e.g., extract a component, rename a type, consolidate duplicate logic) +- After each meaningful change, run tests to confirm nothing broke: + ```bash + npm run test:unit + ``` +- Commit (or note) each stable checkpoint — makes it easy to revert one step without losing all work + +### 4. Common DynamoHome refactors — watch for these risks + +| Refactor | Risk to check | +|---|---| +| Renaming a prop or interface | Are there any callers that use the old name? Run `grep -r "oldName" src/` | +| Moving a component to `Common/` | Update all import paths in components that use it | +| Extracting a custom hook | Verify `useSettings()` context is still available if the hook needs it | +| Consolidating locale keys | Check all 14 locale files if you rename/remove a key; also `grep -r "old.key" src/` to find all usages | +| Changing utility.ts functions | These call `window.chrome.webview.hostObjects.scriptObject` — verify mocks in `tests/__mocks__/chromeMock.ts` cover the new signature | +| Changing SettingsContext shape | All consumers use `useSettings()` — grep for `useSettings` and verify each callsite | + +### 5. Final verification + +```bash +npm run lint:check # No new lint errors +npm run test:unit # All tests pass +npm run build # Dev bundle builds +npm run bundle # Production bundle builds (if webpack config was touched) +``` + +### 6. Do not do during a refactor + +- Do not add new features or fix unrelated bugs — keep the diff focused on the refactor +- Do not introduce new npm dependencies +- Do not change observable behavior (what the user sees or what Dynamo receives) +- Do not rename or remove global window callbacks (`receiveGraphDataFromDotNet`, `setLocale`, etc.) +- Do not change the settings JSON schema (field names, types) +- Do not change the output bundle path + +If you discover a bug while refactoring, note it separately — don't fix it inline (it muddies the refactor diff and makes review harder). diff --git a/README.md b/README.md index f91f2d6..39041d0 100644 --- a/README.md +++ b/README.md @@ -118,3 +118,80 @@ The use of 3rd party libraries was kept to the bare minimum, where developing na - To generate about box html files use `npm run license`, this will output alternative about box files to [license_output](license_output). One will contain the full transitive production dep list, the other will contain the direct production deps. - These files will be packed into the released npm package + +## Claude Code Integration + +This repository includes configuration files for **Claude Code** to assist +with development, testing, and maintenance. + +The `.claude/` directory defines: +- AI agents with specific roles (frontend, testing, build) +- Project-specific knowledge and conventions +- Reusable workflows for common tasks + +### How to use Claude Code + +1. Install and authenticate Claude Code (see Anthropic documentation) +2. Open a terminal at the root of this repository +3. Run: + +```bash +claude +``` + +### Code Review and Pull Request Checks + +A dedicated Claude agent is available for code reviews and pull request checks. + +The `code-review-agent` is designed to: +- Review changes for quality and consistency +- Validate alignment with DynamoHome conventions +- Detect potential regressions or risks +- Provide actionable PR feedback + +#### Example prompts + +- “Use the code-review-agent to review this pull request” +- “Review these changes as a PR and list any issues” +- “Run a PR check using the code-review workflow” + +This helps ensure consistent, high-quality contributions while keeping reviews +focused, incremental, and aligned with project standards. + +### Testing Strategy with Claude Code + +This repository follows a strict testing responsibility model when using Claude Code. + +#### Test folder structure + +``` +tests/ + unit/ # Jest unit tests + App.test.tsx + ComponentName.test.tsx + e2e/ # Playwright end-to-end tests + e2e.test.ts # Orchestration only (no selectors or page actions) + pages/ # Page Object Model — page classes + components/ # Page Object Model — component classes + jest.setup.ts # Jest global setup (chrome mock) + __mocks__/ # Auto-applied mocks (CSS, images, chrome WebView) +``` + +#### Unit Testing +- Unit tests live in `tests/unit/` and are run with `npm run test:unit` +- Every component and module must have unit tests +- The target is 100% unit test coverage +- Missing unit tests must be created when coverage gaps are found + +#### End-to-End Testing (Playwright) +- E2E tests live in `tests/e2e/` and are run with `npm run test:e2e` +- All Playwright tests must follow the Page Object Model (POM) +- Pages and components must be implemented as separate classes in `tests/e2e/pages/` and `tests/e2e/components/` +- `e2e.test.ts` must only contain test orchestration — no selectors or direct page actions + +#### Exploratory Testing +- For exploratory testing or issue investigation, use `playwright-cli open http://localhost:8080` +- Findings from exploratory testing should be converted into formal Page Object classes +- Any feature without test coverage must have new tests added + +This setup ensures consistent test quality, clear ownership, and long-term maintainability. diff --git a/package-lock.json b/package-lock.json index ec37af7..fd34f25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@babel/preset-react": "^7.23.3", "@playwright/test": "^1.27.1", "@testing-library/dom": "^10.3.0", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^15.0.6", "@types/jest": "^29.5.12", "@types/react": "^18.3.3", @@ -45,6 +46,13 @@ "webpack-dev-server": "^5.2.2" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://npm.autodesk.com/artifactory/api/npm/autodesk-npm-virtual/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -3126,6 +3134,33 @@ "node": ">=8" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://npm.autodesk.com/artifactory/api/npm/autodesk-npm-virtual/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://npm.autodesk.com/artifactory/api/npm/autodesk-npm-virtual/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, "node_modules/@testing-library/react": { "version": "15.0.7", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.7.tgz", @@ -5106,6 +5141,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://npm.autodesk.com/artifactory/api/npm/autodesk-npm-virtual/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -7292,6 +7334,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://npm.autodesk.com/artifactory/api/npm/autodesk-npm-virtual/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -10095,6 +10147,16 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://npm.autodesk.com/artifactory/api/npm/autodesk-npm-virtual/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -11230,6 +11292,20 @@ "node": ">= 10.13.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://npm.autodesk.com/artifactory/api/npm/autodesk-npm-virtual/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -12196,6 +12272,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://npm.autodesk.com/artifactory/api/npm/autodesk-npm-virtual/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -12786,7 +12875,7 @@ "version": "5.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index fa12e86..1a6f9bf 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "scripts": { "lint:check": "eslint src/ tests/", "lint:fix": "eslint src/ tests/ --fix", - "test:unit": "NODE_ENV=test & jest tests/App.test.ts", - "test:e2e": "playwright test tests/e2e.test.ts", + "test:unit": "NODE_ENV=test & jest tests/unit", + "test:e2e": "playwright test tests/e2e/e2e.test.ts", "test": "npm run test:unit && npm run test:e2e", "start": "webpack serve --config webpack.config.ts", "build": "webpack --config webpack.config.ts --mode=development", @@ -51,6 +51,7 @@ "@babel/preset-react": "^7.23.3", "@playwright/test": "^1.27.1", "@testing-library/dom": "^10.3.0", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^15.0.6", "@types/jest": "^29.5.12", "@types/react": "^18.3.3", diff --git a/playwright.config.js b/playwright.config.js index dabc4d3..9487dbc 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -13,7 +13,7 @@ const { devices } = require('@playwright/test'); * @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { - testDir: './tests', + testDir: './tests/e2e', /* Maximum time one test can run for. */ timeout: 30 * 1000, expect: { diff --git a/tests/App.test.tsx b/tests/App.test.tsx deleted file mode 100644 index 13b297c..0000000 --- a/tests/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import App from '../src/App'; - -describe('App', () => { - it('renders app successfully', () => { - render(); - }); -}); diff --git a/tests/e2e.test.ts b/tests/e2e/e2e.test.ts similarity index 100% rename from tests/e2e.test.ts rename to tests/e2e/e2e.test.ts diff --git a/tests/jest.setup.ts b/tests/jest.setup.ts index 17548b6..f24bdb9 100644 --- a/tests/jest.setup.ts +++ b/tests/jest.setup.ts @@ -1,3 +1,4 @@ +import '@testing-library/jest-dom'; import { chromeMock } from './__mocks__/chromeMock'; global.chrome = chromeMock; diff --git a/tests/unit/App.test.tsx b/tests/unit/App.test.tsx new file mode 100644 index 0000000..1c096b5 --- /dev/null +++ b/tests/unit/App.test.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { render, screen, act } from '@testing-library/react'; +import App from '../../src/App'; + +jest.mock('react-split-pane', () => { + return function SplitPaneMock({ children }: any) { + return
{children}
; + }; +}); + +describe('App', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders app successfully', () => { + render(); + }); + + it('renders the LayoutContainer with id="homeContainer"', () => { + // LayoutContainer renders a div with className main-container; the id prop is not applied to an HTML element directly + // We verify the app renders and the split-pane (mocked) is present as part of LayoutContainer + render(); + expect(screen.getByTestId('split-pane')).toBeInTheDocument(); + }); + + it('window.setLocale is defined after mount', () => { + render(); + expect(typeof window.setLocale).toBe('function'); + }); + + it('calling window.setLocale does not throw', () => { + render(); + expect(() => { + act(() => { + window.setLocale('es-ES'); + }); + }).not.toThrow(); + }); + + it('calls ApplicationLoaded when chrome.webview exists', () => { + render(); + expect(window.chrome?.webview?.hostObjects?.scriptObject?.ApplicationLoaded).toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/Common/Arrow.test.tsx b/tests/unit/Common/Arrow.test.tsx new file mode 100644 index 0000000..d458c92 --- /dev/null +++ b/tests/unit/Common/Arrow.test.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { ClosedArrow, OpenArrow } from '../../../src/components/Common/Arrow'; + +// CSS modules are mocked as {}, so class names resolve to undefined. +// We test that: +// - isOpen=true adds an extra class entry (array longer by 1) +// - direction adds an extra class entry +// We use getAttribute('class') since SVG.className is SVGAnimatedString, not a plain string. + +describe('ClosedArrow', () => { + it('renders an SVG element', () => { + const { container } = render(); + expect(container.querySelector('svg')).toBeTruthy(); + }); + + it('uses default color #949494 when no color is provided', () => { + const { container } = render(); + const path = container.querySelector('path'); + expect(path?.getAttribute('fill')).toBe('#949494'); + }); + + it('uses the provided color as fill', () => { + const { container } = render(); + const path = container.querySelector('path'); + expect(path?.getAttribute('fill')).toBe('#ff0000'); + }); + + it('SVG has width=8 and height=4', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(svg?.getAttribute('width')).toBe('8'); + expect(svg?.getAttribute('height')).toBe('4'); + }); + + it('isOpen=true adds extra class (class attribute is longer than isOpen=false)', () => { + const { container: c1 } = render(); + const { container: c2 } = render(); + const cls1 = c1.querySelector('svg')?.getAttribute('class') ?? ''; + const cls2 = c2.querySelector('svg')?.getAttribute('class') ?? ''; + expect(cls2.length).toBeGreaterThan(cls1.length); + }); + + it('direction="left" adds extra class compared to no direction', () => { + const { container: c1 } = render(); + const { container: c2 } = render(); + const cls1 = c1.querySelector('svg')?.getAttribute('class') ?? ''; + const cls2 = c2.querySelector('svg')?.getAttribute('class') ?? ''; + expect(cls2.length).toBeGreaterThan(cls1.length); + }); + + it('direction="right" adds extra class compared to no direction', () => { + const { container: c1 } = render(); + const { container: c2 } = render(); + const cls1 = c1.querySelector('svg')?.getAttribute('class') ?? ''; + const cls2 = c2.querySelector('svg')?.getAttribute('class') ?? ''; + expect(cls2.length).toBeGreaterThan(cls1.length); + }); + + it('renders the correct path d attribute', () => { + const { container } = render(); + const path = container.querySelector('path'); + expect(path?.getAttribute('d')).toBe('M4 4L7.5 0H0.5L4 4Z'); + }); +}); + +describe('OpenArrow', () => { + it('renders an SVG element', () => { + const { container } = render(); + expect(container.querySelector('svg')).toBeTruthy(); + }); + + it('uses default color #949494 as stroke when no color provided', () => { + const { container } = render(); + const path = container.querySelector('path'); + expect(path?.getAttribute('stroke')).toBe('#949494'); + }); + + it('uses the provided color as stroke', () => { + const { container } = render(); + const path = container.querySelector('path'); + expect(path?.getAttribute('stroke')).toBe('#00ff00'); + }); + + it('SVG has width=24 and height=24', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(svg?.getAttribute('width')).toBe('24'); + expect(svg?.getAttribute('height')).toBe('24'); + }); + + it('isOpen=true adds extra class (class attribute is longer than isOpen=false)', () => { + const { container: c1 } = render(); + const { container: c2 } = render(); + const cls1 = c1.querySelector('svg')?.getAttribute('class') ?? ''; + const cls2 = c2.querySelector('svg')?.getAttribute('class') ?? ''; + expect(cls2.length).toBeGreaterThan(cls1.length); + }); + + it('direction="left" adds extra class compared to no direction', () => { + const { container: c1 } = render(); + const { container: c2 } = render(); + const cls1 = c1.querySelector('svg')?.getAttribute('class') ?? ''; + const cls2 = c2.querySelector('svg')?.getAttribute('class') ?? ''; + expect(cls2.length).toBeGreaterThan(cls1.length); + }); + + it('direction="right" adds extra class compared to no direction', () => { + const { container: c1 } = render(); + const { container: c2 } = render(); + const cls1 = c1.querySelector('svg')?.getAttribute('class') ?? ''; + const cls2 = c2.querySelector('svg')?.getAttribute('class') ?? ''; + expect(cls2.length).toBeGreaterThan(cls1.length); + }); + + it('path has fill="none"', () => { + const { container } = render(); + const path = container.querySelector('path'); + expect(path?.getAttribute('fill')).toBe('none'); + }); +}); diff --git a/tests/unit/Common/CardItem.test.tsx b/tests/unit/Common/CardItem.test.tsx new file mode 100644 index 0000000..1003819 --- /dev/null +++ b/tests/unit/Common/CardItem.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { CardItem } from '../../../src/components/Common/CardItem'; + +describe('CardItem', () => { + const defaultProps = { + imageSrc: 'test-image.png', + onClick: jest.fn(), + tooltipContent: null, + titleText: 'Test Title', + subtitleText: 'Test Subtitle', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the titleText', () => { + render(); + expect(screen.getByText('Test Title')).toBeInTheDocument(); + }); + + it('renders the subtitleText', () => { + render(); + expect(screen.getByText('Test Subtitle')).toBeInTheDocument(); + }); + + it('renders an img with the provided imageSrc', () => { + const { container } = render(); + const img = container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.getAttribute('src')).toBe('test-image.png'); + }); + + it('calls onClick when the card link is clicked', () => { + render(); + fireEvent.click(screen.getByText('Test Title')); + expect(defaultProps.onClick).toHaveBeenCalled(); + }); + + it('renders without Tooltip when tooltipContent is null', () => { + const { container } = render(); + expect(container.querySelector('.tooltip-wrapper')).toBeNull(); + }); + + it('wraps content in Tooltip when tooltipContent is a string', () => { + const { container } = render(); + expect(container.querySelector('.tooltip-wrapper')).toBeTruthy(); + }); + + it('wraps content in Tooltip when tooltipContent is JSX', () => { + const { container } = render( + JSX tooltip} /> + ); + expect(container.querySelector('.tooltip-wrapper')).toBeTruthy(); + }); +}); diff --git a/tests/unit/Common/CustomIcons.test.tsx b/tests/unit/Common/CustomIcons.test.tsx new file mode 100644 index 0000000..538d19c --- /dev/null +++ b/tests/unit/Common/CustomIcons.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { GridViewIcon, ListViewIcon, QuestionMarkIcon } from '../../../src/components/Common/CustomIcons'; + +describe('GridViewIcon', () => { + it('renders an SVG element', () => { + const { container } = render(); + expect(container.querySelector('svg')).toBeTruthy(); + }); + + it('SVG has correct dimensions', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(svg?.getAttribute('width')).toBe('36'); + expect(svg?.getAttribute('height')).toBe('36'); + }); +}); + +describe('ListViewIcon', () => { + it('renders an SVG element', () => { + const { container } = render(); + expect(container.querySelector('svg')).toBeTruthy(); + }); + + it('SVG has correct dimensions', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(svg?.getAttribute('width')).toBe('36'); + expect(svg?.getAttribute('height')).toBe('36'); + }); +}); + +describe('QuestionMarkIcon', () => { + it('renders an SVG inside a div', () => { + const { container } = render(); + expect(container.querySelector('div')).toBeTruthy(); + expect(container.querySelector('svg')).toBeTruthy(); + }); + + it('SVG has correct dimensions', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(svg?.getAttribute('width')).toBe('16'); + expect(svg?.getAttribute('height')).toBe('16'); + }); +}); diff --git a/tests/unit/Common/Portal.test.tsx b/tests/unit/Common/Portal.test.tsx new file mode 100644 index 0000000..2a6ca4e --- /dev/null +++ b/tests/unit/Common/Portal.test.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import Portal from '../../../src/components/Common/Portal'; + +describe('Portal', () => { + it('renders children into document.body', () => { + render( + +
Portal Content
+
+ ); + expect(document.body.textContent).toContain('Portal Content'); + }); + + it('does NOT render children inside the component container', () => { + const { container } = render( + +
Portal Only Here
+
+ ); + expect(container.textContent).not.toContain('Portal Only Here'); + }); + + it('renders multiple children', () => { + render( + + <> +
Child One
+
Child Two
+ +
+ ); + expect(document.body.textContent).toContain('Child One'); + expect(document.body.textContent).toContain('Child Two'); + }); +}); diff --git a/tests/unit/Common/Tooltip.test.tsx b/tests/unit/Common/Tooltip.test.tsx new file mode 100644 index 0000000..a3b046c --- /dev/null +++ b/tests/unit/Common/Tooltip.test.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { Tooltip } from '../../../src/components/Common/Tooltip'; + +describe('Tooltip', () => { + it('renders children', () => { + const { getByText } = render( + + Child Element + + ); + expect(getByText('Child Element')).toBeInTheDocument(); + }); + + it('tooltip content is not visible initially', () => { + render( + + Hover target + + ); + expect(document.querySelector('.tooltip-box')).toBeNull(); + }); + + it('shows tooltip content after mouseEnter on wrapper', () => { + const { container } = render( + + Hover target + + ); + const wrapper = container.querySelector('.tooltip-wrapper')!; + fireEvent.mouseEnter(wrapper); + expect(document.querySelector('.tooltip-box')).toBeTruthy(); + }); + + it('hides tooltip content after mouseLeave', () => { + const { container } = render( + + Hover target + + ); + const wrapper = container.querySelector('.tooltip-wrapper')!; + fireEvent.mouseEnter(wrapper); + expect(document.querySelector('.tooltip-box')).toBeTruthy(); + fireEvent.mouseLeave(wrapper); + expect(document.querySelector('.tooltip-box')).toBeNull(); + }); + + it('renders children when no content is provided', () => { + const { getByText } = render( + + No tooltip here + + ); + expect(getByText('No tooltip here')).toBeInTheDocument(); + }); +}); diff --git a/tests/unit/LayoutContainer.test.tsx b/tests/unit/LayoutContainer.test.tsx new file mode 100644 index 0000000..7c70f20 --- /dev/null +++ b/tests/unit/LayoutContainer.test.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import { SettingsProvider } from '../../src/components/SettingsContext'; +import { LayoutContainer } from '../../src/components/LayoutContainer'; +import { getMessagesForLocale } from '../../src/localization/localization'; + +jest.mock('react-split-pane', () => { + return function SplitPaneMock({ children, onDragFinished }: any) { + return ( +
+ + {children} +
+ ); + }; +}); + +jest.mock('../../src/components/Sidebar/Sidebar', () => ({ + Sidebar: ({ selectedSidebarItem }: any) => ( +
Sidebar - {selectedSidebarItem}
+ ), +})); + +jest.mock('../../src/components/MainContent', () => ({ + MainContent: ({ isDisabled, selectedSidebarItem }: any) => ( +
+ MainContent - {selectedSidebarItem} +
+ ), +})); + +jest.mock('../../src/functions/utility', () => ({ + saveHomePageSettings: jest.fn(), +})); + +const messages = getMessagesForLocale('en'); + +const renderLayout = () => + render( + + + + + + ); + +describe('LayoutContainer', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders without crash', () => { + expect(() => renderLayout()).not.toThrow(); + }); + + it('renders the Sidebar', () => { + renderLayout(); + expect(screen.getByTestId('sidebar')).toBeInTheDocument(); + }); + + it('renders the MainContent', () => { + renderLayout(); + expect(screen.getByTestId('main-content')).toBeInTheDocument(); + }); + + it('window.setShowStartPageChanged is defined after mount', () => { + renderLayout(); + expect(typeof window.setShowStartPageChanged).toBe('function'); + }); + + it('window.setHomePageSettings is defined after mount', () => { + renderLayout(); + expect(typeof window.setHomePageSettings).toBe('function'); + }); + + it('calling setShowStartPageChanged(false) sets isDisabled=true', () => { + renderLayout(); + act(() => { + window.setShowStartPageChanged!(false); + }); + expect(screen.getByTestId('main-content').getAttribute('data-disabled')).toBe('true'); + }); + + it('calling setShowStartPageChanged(true) sets isDisabled=false', () => { + renderLayout(); + act(() => { + window.setShowStartPageChanged!(false); + }); + act(() => { + window.setShowStartPageChanged!(true); + }); + expect(screen.getByTestId('main-content').getAttribute('data-disabled')).toBe('false'); + }); + + it('calling setHomePageSettings with valid JSON does not throw', () => { + renderLayout(); + expect(() => { + act(() => { + window.setHomePageSettings!('{"recentPageViewMode":"list"}'); + }); + }).not.toThrow(); + }); + + it('calling setHomePageSettings with invalid JSON does not throw', () => { + renderLayout(); + expect(() => { + act(() => { + window.setHomePageSettings!('{invalid-json'); + }); + }).not.toThrow(); + }); + + it('calling setHomePageSettings with empty string does not throw', () => { + renderLayout(); + expect(() => { + act(() => { + window.setHomePageSettings!(''); + }); + }).not.toThrow(); + }); + + it('removes window globals on unmount', () => { + const { unmount } = renderLayout(); + unmount(); + expect(window.setShowStartPageChanged).toBeUndefined(); + expect(window.setHomePageSettings).toBeUndefined(); + }); + + it('triggering resize calls saveHomePageSettings', async () => { + const { saveHomePageSettings } = require('../../src/functions/utility'); + renderLayout(); + await act(async () => { + fireEvent.click(screen.getByTestId('trigger-resize')); + }); + expect(saveHomePageSettings).toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/Learning/Carousel.test.tsx b/tests/unit/Learning/Carousel.test.tsx new file mode 100644 index 0000000..c14f0cc --- /dev/null +++ b/tests/unit/Learning/Carousel.test.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { Carousel } from '../../../src/components/Learning/Carousel'; + +const makeChildren = (count: number) => + Array.from({ length: count }, (_, i) => ( +
+ Child {i} +
+ )); + +describe('Carousel', () => { + it('renders children', () => { + render({makeChildren(3)}); + expect(screen.getByText('Child 0')).toBeInTheDocument(); + expect(screen.getByText('Child 1')).toBeInTheDocument(); + expect(screen.getByText('Child 2')).toBeInTheDocument(); + }); + + it('starts at index 0 with transform translateX(-0%)', () => { + render({makeChildren(6)}); + const carousel = document.getElementById('videoCarousel')!; + expect(carousel.style.transform).toBe('translateX(-0%)'); + }); + + it('clicking right button advances to index 1', () => { + render({makeChildren(6)}); + const carousel = document.getElementById('videoCarousel')!; + const buttons = screen.getAllByRole('button'); + const rightBtn = buttons[1]; // right is second button + fireEvent.click(rightBtn); + expect(carousel.style.transform).toBe('translateX(-25%)'); + }); + + it('clicking right twice advances to index 2', () => { + render({makeChildren(6)}); + const carousel = document.getElementById('videoCarousel')!; + const buttons = screen.getAllByRole('button'); + const rightBtn = buttons[1]; + fireEvent.click(rightBtn); + fireEvent.click(rightBtn); + expect(carousel.style.transform).toBe('translateX(-50%)'); + }); + + it('clicking right at maxIndex wraps to 0', () => { + // 6 items, itemsPerPage=4, maxIndex=2 + render({makeChildren(6)}); + const carousel = document.getElementById('videoCarousel')!; + const buttons = screen.getAllByRole('button'); + const rightBtn = buttons[1]; + // Go to maxIndex (2) + fireEvent.click(rightBtn); + fireEvent.click(rightBtn); + expect(carousel.style.transform).toBe('translateX(-50%)'); + // One more click wraps to 0 + fireEvent.click(rightBtn); + expect(carousel.style.transform).toBe('translateX(-0%)'); + }); + + it('clicking left at index 0 wraps to maxIndex', () => { + // 6 items, maxIndex=2 + render({makeChildren(6)}); + const carousel = document.getElementById('videoCarousel')!; + const buttons = screen.getAllByRole('button'); + const leftBtn = buttons[0]; + fireEvent.click(leftBtn); + expect(carousel.style.transform).toBe('translateX(-50%)'); + }); + + it('clicking left when index > 0 decrements', () => { + render({makeChildren(6)}); + const carousel = document.getElementById('videoCarousel')!; + const buttons = screen.getAllByRole('button'); + const rightBtn = buttons[1]; + const leftBtn = buttons[0]; + // Go to index 2 + fireEvent.click(rightBtn); + fireEvent.click(rightBtn); + // Go back to index 1 + fireEvent.click(leftBtn); + expect(carousel.style.transform).toBe('translateX(-25%)'); + }); + + it('renders with fewer than 4 children without crash', () => { + expect(() => render({makeChildren(2)})).not.toThrow(); + }); + + it('renders left and right navigation buttons', () => { + render({makeChildren(4)}); + const buttons = screen.getAllByRole('button'); + expect(buttons.length).toBe(2); + }); +}); diff --git a/tests/unit/Learning/GuideGridItem.test.tsx b/tests/unit/Learning/GuideGridItem.test.tsx new file mode 100644 index 0000000..a23a5b8 --- /dev/null +++ b/tests/unit/Learning/GuideGridItem.test.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { GuideGridItem } from '../../../src/components/Learning/GuideGridItem'; + +jest.mock('../../../src/functions/utility', () => ({ + startGuidedTour: jest.fn(), +})); + +const { startGuidedTour } = require('../../../src/functions/utility'); + +const defaultProps: Guide = { + id: 'guide-1', + Name: 'Geometry Guide', + Description: 'Learn geometry basics', + Type: 'test', + Thumbnail: '', +}; + +describe('GuideGridItem', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders Name as title', () => { + render(); + expect(screen.getByText('Geometry Guide')).toBeInTheDocument(); + }); + + it('renders Description as subtitle and tooltip', () => { + render(); + expect(screen.getByText('Learn geometry basics')).toBeInTheDocument(); + }); + + it('calls startGuidedTour with Type on click', () => { + const { container } = render(); + fireEvent.click(container.querySelector('a')!); + expect(startGuidedTour).toHaveBeenCalledWith('test'); + }); + + it('uses fallback image when Thumbnail is empty', () => { + const { container } = render(); + const img = container.querySelector('img'); + expect(img?.getAttribute('src')).toMatch(/^data:image\/png;base64,/); + }); + + it('uses Thumbnail URL when provided', () => { + const { container } = render( + + ); + const img = container.querySelector('img'); + expect(img?.getAttribute('src')).toBe('http://example.com/guide-thumb.png'); + }); +}); diff --git a/tests/unit/Learning/ModalItem.test.tsx b/tests/unit/Learning/ModalItem.test.tsx new file mode 100644 index 0000000..274b3d6 --- /dev/null +++ b/tests/unit/Learning/ModalItem.test.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import ModalItem from '../../../src/components/Learning/ModalItem'; + +describe('ModalItem', () => { + beforeEach(() => { + const modalRoot = document.createElement('div'); + modalRoot.setAttribute('id', 'modal-root'); + document.body.appendChild(modalRoot); + }); + + afterEach(() => { + document.getElementById('modal-root')?.remove(); + }); + + it('renders nothing when isOpen=false', () => { + render(
Modal Content
); + expect(screen.queryByText('Modal Content')).not.toBeInTheDocument(); + }); + + it('renders children in the portal when isOpen=true', () => { + render(
Modal Content
); + expect(screen.getByText('Modal Content')).toBeInTheDocument(); + }); + + it('renders a close button when isOpen=true', () => { + render(
Content
); + const closeBtn = screen.getByRole('button', { name: /close/i }); + expect(closeBtn).toBeInTheDocument(); + }); + + it('clicking the close button calls onClose', () => { + const onClose = jest.fn(); + render(
Content
); + const closeBtn = screen.getByRole('button', { name: /close/i }); + fireEvent.click(closeBtn); + expect(onClose).toHaveBeenCalled(); + }); + + it('clicking the overlay calls onClose', () => { + const onClose = jest.fn(); + render( +
Content
+ ); + // The portal renders: ... + // The overlay is the first element child of modal-root (empty div with onClick) + const modalRoot = document.getElementById('modal-root')!; + const overlay = modalRoot.firstElementChild as HTMLElement; + expect(overlay).toBeTruthy(); + fireEvent.click(overlay); + expect(onClose).toHaveBeenCalled(); + }); + + it('renders children in modal-root portal, not in component container', () => { + const { container } = render( +
Portal Content
+ ); + // Content should be in modal-root, not in the direct component container + expect(container.textContent).not.toContain('Portal Content'); + expect(document.getElementById('modal-root')?.textContent).toContain('Portal Content'); + }); +}); diff --git a/tests/unit/Learning/PageLearning.test.tsx b/tests/unit/Learning/PageLearning.test.tsx new file mode 100644 index 0000000..26f6d3e --- /dev/null +++ b/tests/unit/Learning/PageLearning.test.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { render, screen, act } from '@testing-library/react'; +import { renderWithIntl } from '../testUtils'; +import { LearningPage } from '../../../src/components/Learning/PageLearning'; + +jest.mock('../../../src/components/Learning/GuideGridItem', () => ({ + GuideGridItem: ({ Name }: any) =>
{Name}
, +})); + +jest.mock('../../../src/components/Learning/Carousel', () => ({ + Carousel: ({ children }: any) =>
{children}
, +})); + +jest.mock('../../../src/components/Learning/VideoCarouselItem', () => ({ + VideoCarouselItem: ({ title }: any) =>
{title}
, +})); + +const mockGuides: Guide[] = [ + { id: 'g1', Name: 'Guide One', Description: 'Desc 1', Type: '', Thumbnail: '' }, + { id: 'g2', Name: 'Guide Two', Description: 'Desc 2', Type: '', Thumbnail: '' }, +]; + +const mockVideos: VideoCarouselItem[] = [ + { id: 'v1', title: 'Video One', videoId: 'abc123', description: 'Video desc 1' }, + { id: 'v2', title: 'Video Two', videoId: 'def456', description: 'Video desc 2' }, +]; + +describe('LearningPage', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders "Learning" title', () => { + renderWithIntl(); + // learning.title.text.learning = "Learning" + const headings = screen.getAllByText('Learning'); + expect(headings.length).toBeGreaterThan(0); + }); + + it('renders "Interactive Guides" section title', () => { + renderWithIntl(); + expect(screen.getByText('Interactive Guides')).toBeInTheDocument(); + }); + + it('renders "Video Tutorials" section title', () => { + renderWithIntl(); + expect(screen.getByText('Video Tutorials')).toBeInTheDocument(); + }); + + it('renders no guide items without data', () => { + renderWithIntl(); + expect(screen.queryAllByTestId('guide-item').length).toBe(0); + }); + + it('renders no video items without data', () => { + renderWithIntl(); + expect(screen.queryAllByTestId('video-item').length).toBe(0); + }); + + it('window.receiveInteractiveGuidesDataFromDotNet is defined after mount', () => { + renderWithIntl(); + expect(typeof window.receiveInteractiveGuidesDataFromDotNet).toBe('function'); + }); + + it('window.receiveTrainingVideoDataFromDotNet is defined after mount', () => { + renderWithIntl(); + expect(typeof window.receiveTrainingVideoDataFromDotNet).toBe('function'); + }); + + it('receiveInteractiveGuidesDataFromDotNet populates guide items', () => { + renderWithIntl(); + act(() => { + window.receiveInteractiveGuidesDataFromDotNet(mockGuides); + }); + expect(screen.getByText('Guide One')).toBeInTheDocument(); + expect(screen.getByText('Guide Two')).toBeInTheDocument(); + }); + + it('receiveTrainingVideoDataFromDotNet populates video items', () => { + renderWithIntl(); + act(() => { + window.receiveTrainingVideoDataFromDotNet(mockVideos); + }); + expect(screen.getByText('Video One')).toBeInTheDocument(); + expect(screen.getByText('Video Two')).toBeInTheDocument(); + }); + + it('receiveInteractiveGuidesDataFromDotNet does not crash with empty array', () => { + renderWithIntl(); + expect(() => { + act(() => { + window.receiveInteractiveGuidesDataFromDotNet([]); + }); + }).not.toThrow(); + }); + + it('receiveTrainingVideoDataFromDotNet does not crash with empty array', () => { + renderWithIntl(); + expect(() => { + act(() => { + window.receiveTrainingVideoDataFromDotNet([]); + }); + }).not.toThrow(); + }); + + it('removes window globals on unmount', () => { + const { unmount } = renderWithIntl(); + unmount(); + expect(window.receiveInteractiveGuidesDataFromDotNet).toBeUndefined(); + expect(window.receiveTrainingVideoDataFromDotNet).toBeUndefined(); + }); +}); diff --git a/tests/unit/Learning/VideoCarouselItem.test.tsx b/tests/unit/Learning/VideoCarouselItem.test.tsx new file mode 100644 index 0000000..2ae793d --- /dev/null +++ b/tests/unit/Learning/VideoCarouselItem.test.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { VideoCarouselItem } from '../../../src/components/Learning/VideoCarouselItem'; + +describe('VideoCarouselItem', () => { + beforeEach(() => { + const modalRoot = document.createElement('div'); + modalRoot.setAttribute('id', 'modal-root'); + document.body.appendChild(modalRoot); + }); + + afterEach(() => { + document.getElementById('modal-root')?.remove(); + }); + + const defaultProps: VideoCarouselItem = { + id: 'v1', + title: 'Intro to Dynamo', + videoId: 'abc123xyz', + description: 'A great intro video', + }; + + it('renders the title', () => { + render(); + expect(screen.getByText('Intro to Dynamo')).toBeInTheDocument(); + }); + + it('renders the description', () => { + render(); + expect(screen.getByText('A great intro video')).toBeInTheDocument(); + }); + + it('renders an iframe with the correct YouTube embed URL', () => { + const { container } = render(); + const iframes = container.querySelectorAll('iframe'); + const youtubeUrl = `https://www.youtube.com/embed/abc123xyz?autoplay=1`; + const hasYoutubeIframe = Array.from(iframes).some( + (iframe) => iframe.getAttribute('src') === youtubeUrl + ); + expect(hasYoutubeIframe).toBe(true); + }); + + it('shows video overlay div initially (empty div sibling to iframe)', () => { + const { container } = render(); + // The iframe is inside the clipped-video-container + const iframe = container.querySelector('iframe')!; + const clippedContainer = iframe.parentElement!; + // Initially: overlay div + iframe = 2 children + expect(clippedContainer.children.length).toBe(2); + // The first child (overlay) has no content + expect(clippedContainer.firstElementChild?.tagName).toBe('DIV'); + }); + + it('clicking the video container removes the overlay', () => { + const { container } = render(); + const iframe = container.querySelector('iframe')!; + const clippedContainer = iframe.parentElement as HTMLElement; + fireEvent.click(clippedContainer); + // After modal opens, overlay is removed: only iframe remains in clipped-video-container + expect(clippedContainer.children.length).toBe(1); + }); + + it('clicking the video container shows modal with close button', () => { + const { container } = render(); + const iframe = container.querySelector('iframe')!; + const clippedContainer = iframe.parentElement as HTMLElement; + fireEvent.click(clippedContainer); + const closeBtn = screen.getByRole('button', { name: /close/i }); + expect(closeBtn).toBeInTheDocument(); + }); + + it('closing the modal restores the overlay div', () => { + const { container } = render(); + const iframe = container.querySelector('iframe')!; + const clippedContainer = iframe.parentElement as HTMLElement; + // Open modal + fireEvent.click(clippedContainer); + expect(clippedContainer.children.length).toBe(1); + // Close modal + const closeBtn = screen.getByRole('button', { name: /close/i }); + fireEvent.click(closeBtn); + // Overlay returns + expect(clippedContainer.children.length).toBe(2); + }); +}); diff --git a/tests/unit/MainContent.test.tsx b/tests/unit/MainContent.test.tsx new file mode 100644 index 0000000..fec886e --- /dev/null +++ b/tests/unit/MainContent.test.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import { getMessagesForLocale } from '../../src/localization/localization'; +import { MainContent } from '../../src/components/MainContent'; + +jest.mock('../../src/components/Recent/PageRecent', () => ({ + RecentPage: () =>
Recent Page
, +})); +jest.mock('../../src/components/Samples/PageSamples', () => ({ + SamplesPage: () =>
Samples Page
, +})); +jest.mock('../../src/components/Learning/PageLearning', () => ({ + LearningPage: () =>
Learning Page
, +})); + +const messages = getMessagesForLocale('en'); + +const renderContent = (props: Partial = {}) => { + const defaultProps: MainContentProps = { + selectedSidebarItem: 'Recent', + settings: null, + isDisabled: false, + setIsDisabled: jest.fn(), + ...props, + }; + return render( + + + + ); +}; + +describe('MainContent', () => { + it('renders Recent page when selectedSidebarItem="Recent"', () => { + renderContent({ selectedSidebarItem: 'Recent' }); + expect(screen.getByTestId('recent-page')).toBeInTheDocument(); + }); + + it('Recent page container does not have "hidden" class when selected', () => { + const { container } = renderContent({ selectedSidebarItem: 'Recent' }); + const pageContainers = container.querySelectorAll('.page-container'); + // First page-container is Recent + expect(pageContainers[0].classList.contains('hidden')).toBe(false); + }); + + it('Samples and Learning containers have "hidden" class when Recent is selected', () => { + const { container } = renderContent({ selectedSidebarItem: 'Recent' }); + const pageContainers = container.querySelectorAll('.page-container'); + expect(pageContainers[1].classList.contains('hidden')).toBe(true); + expect(pageContainers[2].classList.contains('hidden')).toBe(true); + }); + + it('renders Samples page container without "hidden" when selectedSidebarItem="Samples"', () => { + const { container } = renderContent({ selectedSidebarItem: 'Samples' }); + const pageContainers = container.querySelectorAll('.page-container'); + expect(pageContainers[1].classList.contains('hidden')).toBe(false); + expect(pageContainers[0].classList.contains('hidden')).toBe(true); + expect(pageContainers[2].classList.contains('hidden')).toBe(true); + }); + + it('renders Learning page container without "hidden" when selectedSidebarItem="Learning"', () => { + const { container } = renderContent({ selectedSidebarItem: 'Learning' }); + const pageContainers = container.querySelectorAll('.page-container'); + expect(pageContainers[2].classList.contains('hidden')).toBe(false); + expect(pageContainers[0].classList.contains('hidden')).toBe(true); + expect(pageContainers[1].classList.contains('hidden')).toBe(true); + }); + + it('shows loading overlay with "Loading" text when isDisabled=true', () => { + renderContent({ isDisabled: true }); + expect(screen.getByText('Loading')).toBeInTheDocument(); + expect(document.querySelector('.loading-overlay')).toBeTruthy(); + }); + + it('does not show loading overlay when isDisabled=false', () => { + renderContent({ isDisabled: false }); + expect(document.querySelector('.loading-overlay')).toBeNull(); + expect(screen.queryByText('Loading')).not.toBeInTheDocument(); + }); + + it('passes settings.recentPageViewMode to RecentPage', () => { + renderContent({ settings: { recentPageViewMode: 'list', samplesViewMode: undefined, sideBarWidth: undefined } }); + // RecentPage is mocked so this just verifies no crash + expect(screen.getByTestId('recent-page')).toBeInTheDocument(); + }); +}); diff --git a/tests/unit/Recent/CustomAuthorCellRenderer.test.tsx b/tests/unit/Recent/CustomAuthorCellRenderer.test.tsx new file mode 100644 index 0000000..808f02d --- /dev/null +++ b/tests/unit/Recent/CustomAuthorCellRenderer.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { renderWithIntl } from '../testUtils'; +import { CustomAuthorCellRenderer } from '../../../src/components/Recent/CustomAuthorCellRenderer'; + +describe('CustomAuthorCellRenderer', () => { + it('renders the author name', () => { + renderWithIntl(); + expect(screen.getByText('John Doe')).toBeInTheDocument(); + }); + + it('shows QuestionMarkIcon when value is "Dynamo 1.x file format"', () => { + const { container } = renderWithIntl( + + ); + // QuestionMarkIcon renders an SVG inside a div + const svg = container.querySelector('svg'); + expect(svg).toBeTruthy(); + }); + + it('does NOT show QuestionMarkIcon for regular author names', () => { + const { container } = renderWithIntl( + + ); + // Should not render QuestionMarkIcon (no tooltip-wrapper for the icon) + const svgs = container.querySelectorAll('svg'); + expect(svgs.length).toBe(0); + }); + + it('shows Tooltip with old format explanation when isOldFormat', () => { + const { container } = renderWithIntl( + + ); + expect(container.querySelector('.tooltip-wrapper')).toBeTruthy(); + }); + + it('does NOT show Tooltip when author is a regular name', () => { + const { container } = renderWithIntl( + + ); + expect(container.querySelector('.tooltip-wrapper')).toBeNull(); + }); +}); diff --git a/tests/unit/Recent/CustomLocationCellRenderer.test.tsx b/tests/unit/Recent/CustomLocationCellRenderer.test.tsx new file mode 100644 index 0000000..bae151b --- /dev/null +++ b/tests/unit/Recent/CustomLocationCellRenderer.test.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { CustomLocationCellRenderer } from '../../../src/components/Recent/CustomLocationCellRenderer'; + +describe('CustomLocationCellRenderer', () => { + it('renders the value (file path)', () => { + render(); + expect(screen.getByText('/some/path/file.dyn')).toBeInTheDocument(); + }); + + it('wraps the value in a Tooltip', () => { + const { container } = render( + + ); + expect(container.querySelector('.tooltip-wrapper')).toBeTruthy(); + }); + + it('tooltip shows the path on mouseEnter', () => { + const { container } = render( + + ); + const wrapper = container.querySelector('.tooltip-wrapper')!; + fireEvent.mouseEnter(wrapper); + // Tooltip renders content to document.body via Portal + expect(document.body.textContent).toContain('/some/path/to/file.dyn'); + }); +}); diff --git a/tests/unit/Recent/CustomNameCellRenderer.test.tsx b/tests/unit/Recent/CustomNameCellRenderer.test.tsx new file mode 100644 index 0000000..6cd9545 --- /dev/null +++ b/tests/unit/Recent/CustomNameCellRenderer.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { CustomNameCellRenderer } from '../../../src/components/Recent/CustomNameCellRenderer'; + +const makeRow = (overrides: any = {}) => ({ + original: { + Thumbnail: null, + Description: '', + ...overrides, + }, +}); + +describe('CustomNameCellRenderer', () => { + it('renders the value (graph name)', () => { + render(); + expect(screen.getByText('My Graph')).toBeInTheDocument(); + }); + + it('wraps name in Tooltip when Description exists', () => { + const { container } = render( + + ); + expect(container.querySelector('.tooltip-wrapper')).toBeTruthy(); + }); + + it('does NOT wrap in Tooltip when Description is empty', () => { + const { container } = render( + + ); + expect(container.querySelector('.tooltip-wrapper')).toBeNull(); + }); + + it('uses fallback image when no Thumbnail', () => { + const { container } = render( + + ); + const img = container.querySelector('img'); + expect(img?.getAttribute('src')).toMatch(/^data:image\/png;base64,/); + }); + + it('uses Thumbnail URL when provided', () => { + const { container } = render( + + ); + const img = container.querySelector('img'); + expect(img?.getAttribute('src')).toBe('http://example.com/thumb.png'); + }); + + it('tooltip shows description on mouseEnter', () => { + const { container } = render( + + ); + const wrapper = container.querySelector('.tooltip-wrapper')!; + fireEvent.mouseEnter(wrapper); + expect(document.body.textContent).toContain('Graph description'); + }); +}); diff --git a/tests/unit/Recent/GraphGridItem.test.tsx b/tests/unit/Recent/GraphGridItem.test.tsx new file mode 100644 index 0000000..ecd5200 --- /dev/null +++ b/tests/unit/Recent/GraphGridItem.test.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { GraphGridItem } from '../../../src/components/Recent/GraphGridItem'; + +jest.mock('../../../src/functions/utility', () => ({ + openFile: jest.fn(), + saveHomePageSettings: jest.fn(), +})); + +const { openFile } = require('../../../src/functions/utility'); + +const defaultProps = { + id: 'graph-1', + Caption: 'My Graph', + ContextData: '/path/to/graph.dyn', + Description: 'A test graph', + DateModified: '2024-01-15', + Thumbnail: null, + setIsDisabled: jest.fn(), +}; + +describe('GraphGridItem', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the Caption as title', () => { + render(); + expect(screen.getByText('My Graph')).toBeInTheDocument(); + }); + + it('renders the DateModified as subtitle', () => { + render(); + expect(screen.getByText('2024-01-15')).toBeInTheDocument(); + }); + + it('calls setIsDisabled(true) and openFile on click', () => { + const { container } = render(); + fireEvent.click(container.querySelector('a')!); + expect(defaultProps.setIsDisabled).toHaveBeenCalledWith(true); + expect(openFile).toHaveBeenCalledWith('/path/to/graph.dyn'); + }); + + it('uses fallback image when no Thumbnail is provided', () => { + const { container } = render(); + const img = container.querySelector('img'); + // fallback img is a base64 PNG embedded in src/assets/home.ts + expect(img?.getAttribute('src')).toMatch(/^data:image\/png;base64,/); + }); + + it('uses Thumbnail URL when Thumbnail is provided', () => { + const { container } = render(); + const img = container.querySelector('img'); + expect(img?.getAttribute('src')).toBe('http://example.com/thumb.png'); + }); +}); diff --git a/tests/unit/Recent/GraphTable.test.tsx b/tests/unit/Recent/GraphTable.test.tsx new file mode 100644 index 0000000..db77160 --- /dev/null +++ b/tests/unit/Recent/GraphTable.test.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { GraphTable } from '../../../src/components/Recent/GraphTable'; + +// Suppress the console.log(headerGroups) inside GraphTable +beforeAll(() => { + jest.spyOn(console, 'log').mockImplementation(() => {}); +}); +afterAll(() => { + (console.log as jest.Mock).mockRestore(); +}); + +const columns: Column[] = [ + { Header: 'Title', accessor: 'Caption', resizable: true }, + { Header: 'Author', accessor: 'Author', resizable: true }, + { Header: 'Date Modified', accessor: 'DateModified', resizable: true }, + { Header: 'Location', accessor: 'ContextData', resizable: true }, +]; + +const mockData: any[] = [ + { + id: '1', + date: '2024-01-01', + Caption: 'Graph One', + Author: 'Alice', + DateModified: '2024-01-01', + ContextData: '/path/one.dyn', + Thumbnail: '', + }, + { + id: '2', + date: '2024-01-02', + Caption: 'Graph Two', + Author: 'Bob', + DateModified: '2024-01-02', + ContextData: '/path/two.dyn', + Thumbnail: '', + }, +]; + +describe('GraphTable', () => { + it('renders column headers', () => { + render(); + expect(screen.getByText('Title')).toBeInTheDocument(); + expect(screen.getByText('Author')).toBeInTheDocument(); + expect(screen.getByText('Date Modified')).toBeInTheDocument(); + expect(screen.getByText('Location')).toBeInTheDocument(); + }); + + it('renders a row for each data item', () => { + render(); + expect(screen.getByText('Graph One')).toBeInTheDocument(); + expect(screen.getByText('Graph Two')).toBeInTheDocument(); + }); + + it('calls onRowClick when a row is clicked', () => { + const onRowClick = jest.fn(); + render(); + // Click the first row + const rows = screen.getAllByRole('row'); + // rows[0] is the header row, rows[1] is first data row + fireEvent.click(rows[1]); + expect(onRowClick).toHaveBeenCalled(); + }); + + it('renders no data rows when data is empty', () => { + render(); + expect(screen.queryByText('Graph One')).not.toBeInTheDocument(); + // Header row is still present + expect(screen.getByText('Title')).toBeInTheDocument(); + }); +}); diff --git a/tests/unit/Recent/PageRecent.test.tsx b/tests/unit/Recent/PageRecent.test.tsx new file mode 100644 index 0000000..8c46dbe --- /dev/null +++ b/tests/unit/Recent/PageRecent.test.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { screen, fireEvent, act } from '@testing-library/react'; +import { renderWithProviders } from '../testUtils'; +import { RecentPage } from '../../../src/components/Recent/PageRecent'; + +jest.mock('../../../src/components/Recent/GraphGridItem', () => ({ + GraphGridItem: ({ Caption }: any) => ( +
{Caption}
+ ), +})); + +jest.mock('../../../src/components/Recent/GraphTable', () => ({ + GraphTable: ({ data, onRowClick }: any) => ( +
+ {data.map((d: any) => ( +
onRowClick({ original: d })}> + {d.Caption} +
+ ))} +
+ ), +})); + +jest.mock('../../../src/functions/utility', () => ({ + openFile: jest.fn(), + saveHomePageSettings: jest.fn(), +})); + +const defaultProps = { + setIsDisabled: jest.fn(), + recentPageViewMode: 'grid' as const, +}; + +const mockGraphs = [ + { id: '1', Caption: 'Graph One', ContextData: '/path/one.dyn', DateModified: '2024-01-01', Thumbnail: null, Description: '' }, + { id: '2', Caption: 'Graph Two', ContextData: '/path/two.dyn', DateModified: '2024-01-02', Thumbnail: null, Description: '' }, +]; + +describe('RecentPage', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders "Recent" title', () => { + renderWithProviders(); + expect(screen.getByText('Recent')).toBeInTheDocument(); + }); + + it('renders grid view button', () => { + const { container } = renderWithProviders(); + const buttons = container.querySelectorAll('button.viewmode-button'); + expect(buttons.length).toBeGreaterThanOrEqual(2); + }); + + it('defaults to grid mode: shows graph grid, not table', () => { + renderWithProviders(); + expect(document.getElementById('graphContainer')).toBeTruthy(); + expect(screen.queryByTestId('graph-table')).not.toBeInTheDocument(); + }); + + it('list mode shows table, not grid', () => { + renderWithProviders(); + expect(screen.getByTestId('graph-table')).toBeInTheDocument(); + expect(document.getElementById('graphContainer')).toBeNull(); + }); + + it('clicking list button changes to list mode', () => { + const { container } = renderWithProviders(); + const buttons = container.querySelectorAll('button.viewmode-button'); + // Second button is list + fireEvent.click(buttons[1]); + expect(screen.getByTestId('graph-table')).toBeInTheDocument(); + }); + + it('clicking grid button changes to grid mode', () => { + const { container } = renderWithProviders(); + const buttons = container.querySelectorAll('button.viewmode-button'); + // First button is grid + fireEvent.click(buttons[0]); + expect(document.getElementById('graphContainer')).toBeTruthy(); + }); + + it('grid button is disabled when viewMode is grid', () => { + const { container } = renderWithProviders(); + const gridBtn = container.querySelectorAll('button.viewmode-button')[0]; + expect(gridBtn).toBeDisabled(); + }); + + it('list button is disabled when viewMode is list', () => { + const { container } = renderWithProviders(); + const listBtn = container.querySelectorAll('button.viewmode-button')[1]; + expect(listBtn).toBeDisabled(); + }); + + it('window.receiveGraphDataFromDotNet is defined after mount', () => { + renderWithProviders(); + expect(typeof window.receiveGraphDataFromDotNet).toBe('function'); + }); + + it('window.receiveGraphDataFromDotNet updates displayed graphs', async () => { + renderWithProviders(); + act(() => { + window.receiveGraphDataFromDotNet(mockGraphs); + }); + expect(screen.getByText('Graph One')).toBeInTheDocument(); + expect(screen.getByText('Graph Two')).toBeInTheDocument(); + }); + + it('receiveGraphDataFromDotNet with empty array does not crash', () => { + renderWithProviders(); + expect(() => { + act(() => { + window.receiveGraphDataFromDotNet([]); + }); + }).not.toThrow(); + }); + + it('window.receiveGraphDataFromDotNet is removed on unmount', () => { + const { unmount } = renderWithProviders(); + unmount(); + expect(window.receiveGraphDataFromDotNet).toBeUndefined(); + }); +}); diff --git a/tests/unit/Samples/CustomSampleFirstCellRenderer.test.tsx b/tests/unit/Samples/CustomSampleFirstCellRenderer.test.tsx new file mode 100644 index 0000000..ab931f8 --- /dev/null +++ b/tests/unit/Samples/CustomSampleFirstCellRenderer.test.tsx @@ -0,0 +1,180 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { CustomSampleFirstCellRenderer } from '../../../src/components/Samples/CustomSampleFirstCellRenderer'; + +// Cast so TypeScript doesn't complain about the string return case +const Renderer = CustomSampleFirstCellRenderer as React.FC; + +const makeRow = (overrides: Partial = {}): Row => ({ + original: { + id: 'row-1', + parentId: null, + isParent: false, + depth: 0, + ContextData: '', + ...overrides, + } as Original, + cells: [], + getRowProps: (p) => p, +}); + +describe('CustomSampleFirstCellRenderer', () => { + describe('leaf node (no isParent, parentId=null)', () => { + it('returns the value directly as text', () => { + const row = makeRow({ parentId: null, isParent: false }); + render(); + expect(screen.getByText('Leaf Value')).toBeInTheDocument(); + }); + + it('does not render dashed border divs', () => { + const row = makeRow({ parentId: null, isParent: false }); + const { container } = render( + + ); + // Leaf case returns just the value string - no wrapping divs with borders + expect(container.querySelector('[style*="dashed"]')).toBeNull(); + }); + }); + + describe('parent row (isParent=true, parentId=null)', () => { + it('renders the value', () => { + const row = makeRow({ parentId: null, isParent: true }); + render(); + expect(screen.getByText('Parent Section')).toBeInTheDocument(); + }); + + it('renders a ClosedArrow (SVG)', () => { + const row = makeRow({ parentId: null, isParent: true }); + const { container } = render( + + ); + expect(container.querySelector('svg')).toBeTruthy(); + }); + + it('ClosedArrow class is longer (isOpen=true) when row id NOT in collapsedRows', () => { + const row = makeRow({ id: 'parent-1', parentId: null, isParent: true }); + // isOpen=true => extra class appended + const { container: c1 } = render( + + ); + // isOpen=false => no extra class + const { container: c2 } = render( + + ); + const cls1 = c1.querySelector('svg')?.getAttribute('class') ?? ''; + const cls2 = c2.querySelector('svg')?.getAttribute('class') ?? ''; + // isOpen=true should have more class tokens + expect(cls1.length).toBeGreaterThan(cls2.length); + }); + + it('ClosedArrow class is shorter (isOpen=false) when row id IS in collapsedRows', () => { + const row = makeRow({ id: 'parent-1', parentId: null, isParent: true }); + const { container } = render( + + ); + // When collapsed, isOpen=false → fewer class tokens + const svg = container.querySelector('svg'); + expect(svg).toBeTruthy(); + }); + }); + + describe('child row (isChildRow=true, isParent=false)', () => { + const parentRow = makeRow({ id: 'parent-1', parentId: null, isParent: true, depth: 1 }); + const childRow = makeRow({ id: 'child-1', parentId: 'parent-1', isParent: false, depth: 2 }); + + it('renders the value', () => { + render( + + ); + expect(screen.getByText('Child Value')).toBeInTheDocument(); + }); + + it('renders dashed border divs', () => { + const { container } = render( + + ); + expect(container.querySelector('[style*="dashed"]')).toBeTruthy(); + }); + + it('last child has borderStyle bottom=14', () => { + // childRow is last row (rowIndex=1 = rows.length-1) + const { container } = render( + + ); + // Find the absolute-positioned dashed border div + const borderDiv = container.querySelector('[style*="border-left"]'); + expect(borderDiv).toBeTruthy(); + // bottom: 14 is applied as isLastChild=true + expect(borderDiv?.getAttribute('style')).toContain('bottom: 14px'); + }); + + it('non-last child has borderStyle bottom=0', () => { + const siblingRow = makeRow({ id: 'child-2', parentId: 'parent-1', isParent: false, depth: 2 }); + const { container } = render( + + ); + const borderDiv = container.querySelector('[style*="border-left"]'); + expect(borderDiv).toBeTruthy(); + // bottom: 0 is applied as isLastChild=false + const style = borderDiv?.getAttribute('style') ?? ''; + // top: 0; bottom: 0 means bottom is 0px or 0 + expect(style).toMatch(/bottom:\s*0(px)?/); + }); + }); + + describe('nested parent row (isChildRow=true, isParent=true)', () => { + const parentRow = makeRow({ id: 'parent-1', parentId: null, isParent: true, depth: 1 }); + const nestedParent = makeRow({ id: 'nested-1', parentId: 'parent-1', isParent: true, depth: 2 }); + + it('renders the value', () => { + render( + + ); + expect(screen.getByText('Nested Section')).toBeInTheDocument(); + }); + + it('renders dashed border and an arrow (SVG)', () => { + const { container } = render( + + ); + expect(container.querySelector('[style*="dashed"]')).toBeTruthy(); + expect(container.querySelector('svg')).toBeTruthy(); + }); + }); +}); diff --git a/tests/unit/Samples/PageSamples.test.tsx b/tests/unit/Samples/PageSamples.test.tsx new file mode 100644 index 0000000..25219c4 --- /dev/null +++ b/tests/unit/Samples/PageSamples.test.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { screen, fireEvent, act } from '@testing-library/react'; +import { renderWithProviders } from '../testUtils'; +import { SamplesPage } from '../../../src/components/Samples/PageSamples'; + +jest.mock('../../../src/components/Samples/SamplesTable', () => ({ + SamplesTable: ({ data, onRowClick }: any) => ( +
+ {data.map((d: any, i: number) => ( +
onRowClick(d)}> + {d.FileName} +
+ ))} +
+ ), +})); + +jest.mock('../../../src/components/Samples/SamplesGrid', () => ({ + SamplesGrid: ({ data }: any) =>
SamplesGrid
, +})); + +jest.mock('../../../src/functions/utility', () => ({ + openFile: jest.fn(), + showSamplesCommand: jest.fn(), + saveHomePageSettings: jest.fn(), +})); + +const { openFile, showSamplesCommand } = require('../../../src/functions/utility'); + +const mockSamples = [ + { FileName: 'Sample One', FilePath: '/path/one.dyn', Description: '', DateModified: '2024-01-01', Thumbnail: '' }, +]; + +describe('SamplesPage', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders "Samples" title', () => { + renderWithProviders(); + expect(screen.getByText('Samples')).toBeInTheDocument(); + }); + + it('grid mode is default: shows SamplesGrid', () => { + renderWithProviders(); + expect(screen.getByTestId('samples-grid')).toBeInTheDocument(); + expect(screen.queryByTestId('samples-table')).not.toBeInTheDocument(); + }); + + it('list mode shows SamplesTable', () => { + renderWithProviders(); + expect(screen.getByTestId('samples-table')).toBeInTheDocument(); + expect(screen.queryByTestId('samples-grid')).not.toBeInTheDocument(); + }); + + it('clicking List button changes to list mode', () => { + const { container } = renderWithProviders(); + const buttons = container.querySelectorAll('button.viewmode-button'); + fireEvent.click(buttons[1]); // list button + expect(screen.getByTestId('samples-table')).toBeInTheDocument(); + }); + + it('clicking Grid button changes to grid mode', () => { + const { container } = renderWithProviders(); + const buttons = container.querySelectorAll('button.viewmode-button'); + fireEvent.click(buttons[0]); // grid button + expect(screen.getByTestId('samples-grid')).toBeInTheDocument(); + }); + + it('grid button is disabled when viewMode is grid', () => { + const { container } = renderWithProviders(); + const gridBtn = container.querySelectorAll('button.viewmode-button')[0]; + expect(gridBtn).toBeDisabled(); + }); + + it('list button is disabled when viewMode is list', () => { + const { container } = renderWithProviders(); + const listBtn = container.querySelectorAll('button.viewmode-button')[1]; + expect(listBtn).toBeDisabled(); + }); + + it('window.receiveSamplesDataFromDotNet is defined after mount', () => { + renderWithProviders(); + expect(typeof window.receiveSamplesDataFromDotNet).toBe('function'); + }); + + it('window.receiveSamplesDataFromDotNet does not crash with null data', () => { + renderWithProviders(); + expect(() => { + act(() => { + window.receiveSamplesDataFromDotNet(null); + }); + }).not.toThrow(); + }); + + it('window.receiveSamplesDataFromDotNet is removed on unmount', () => { + const { unmount } = renderWithProviders(); + unmount(); + expect(window.receiveSamplesDataFromDotNet).toBeUndefined(); + }); + + it('renders the "Open file location" dropdown', () => { + renderWithProviders(); + expect(screen.getByText('Open file location')).toBeInTheDocument(); + }); +}); diff --git a/tests/unit/Samples/SamplesGrid.test.tsx b/tests/unit/Samples/SamplesGrid.test.tsx new file mode 100644 index 0000000..6cb5332 --- /dev/null +++ b/tests/unit/Samples/SamplesGrid.test.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { SamplesGrid } from '../../../src/components/Samples/SamplesGrid'; + +jest.mock('../../../src/components/Samples/SamplesGridItem', () => ({ + SamplesGridItem: ({ FileName }: any) =>
{FileName}
, +})); + +const makeLeaf = (name: string, path = '/path'): Samples => ({ + FileName: name, + FilePath: path, + Description: '', + DateModified: '2024-01-01', + Thumbnail: '', + Children: [], +}); + +const makeParent = (name: string, children: Samples[]): Samples => ({ + FileName: name, + FilePath: '', + Description: '', + DateModified: '', + Thumbnail: '', + Children: children, +}); + +describe('SamplesGrid', () => { + it('renders items from root children', () => { + const data: Samples[] = [ + makeParent('Root', [ + makeLeaf('Leaf One'), + makeLeaf('Leaf Two'), + ]), + ]; + render(); + expect(screen.getByText('Leaf One')).toBeInTheDocument(); + expect(screen.getByText('Leaf Two')).toBeInTheDocument(); + }); + + it('renders leaf node as SamplesGridItem', () => { + const data: Samples[] = [ + makeParent('Root', [makeLeaf('Leaf Item')]), + ]; + render(); + expect(screen.getAllByTestId('sample-grid-item').length).toBeGreaterThan(0); + }); + + it('renders parent node with section title and its leaf children', () => { + const data: Samples[] = [ + makeParent('Root', [ + makeParent('Section A', [ + makeLeaf('Child One'), + makeLeaf('Child Two'), + ]), + ]), + ]; + render(); + expect(screen.getByText('Section A')).toBeInTheDocument(); + expect(screen.getByText('Child One')).toBeInTheDocument(); + expect(screen.getByText('Child Two')).toBeInTheDocument(); + }); + + it('renders nested parent nodes recursively', () => { + const data: Samples[] = [ + makeParent('Root', [ + makeParent('Level 1', [ + makeParent('Level 2', [ + makeLeaf('Deep Leaf'), + ]), + ]), + ]), + ]; + render(); + expect(screen.getByText('Level 1')).toBeInTheDocument(); + expect(screen.getByText('Level 2')).toBeInTheDocument(); + expect(screen.getByText('Deep Leaf')).toBeInTheDocument(); + }); + + it('handles empty data array without crash', () => { + expect(() => render()).not.toThrow(); + }); + + it('handles data[0] without Children without crash', () => { + const data: Samples[] = [ + { FileName: 'Root', FilePath: '', Description: '', DateModified: '', Thumbnail: '' }, + ]; + expect(() => render()).not.toThrow(); + }); + + it('renders the samplesContainer element', () => { + render(); + expect(document.getElementById('samplesContainer')).toBeTruthy(); + }); +}); diff --git a/tests/unit/Samples/SamplesGridItem.test.tsx b/tests/unit/Samples/SamplesGridItem.test.tsx new file mode 100644 index 0000000..c05b7ef --- /dev/null +++ b/tests/unit/Samples/SamplesGridItem.test.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { SamplesGridItem } from '../../../src/components/Samples/SamplesGridItem'; + +jest.mock('../../../src/functions/utility', () => ({ + openFile: jest.fn(), +})); + +const { openFile } = require('../../../src/functions/utility'); + +const defaultProps: Samples = { + FileName: 'Sample Graph', + FilePath: '/path/sample.dyn', + Description: 'A sample description', + DateModified: '2024-03-01', + Thumbnail: '', +}; + +describe('SamplesGridItem', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders FileName as title', () => { + render(); + expect(screen.getByText('Sample Graph')).toBeInTheDocument(); + }); + + it('renders DateModified as subtitle', () => { + render(); + expect(screen.getByText('2024-03-01')).toBeInTheDocument(); + }); + + it('calls openFile with FilePath on click', () => { + const { container } = render(); + fireEvent.click(container.querySelector('a')!); + expect(openFile).toHaveBeenCalledWith('/path/sample.dyn'); + }); + + it('uses fallback image when Thumbnail is empty', () => { + const { container } = render(); + const img = container.querySelector('img'); + expect(img?.getAttribute('src')).toMatch(/^data:image\/png;base64,/); + }); + + it('uses Thumbnail URL when provided', () => { + const { container } = render( + + ); + const img = container.querySelector('img'); + expect(img?.getAttribute('src')).toBe('http://example.com/thumb.png'); + }); +}); diff --git a/tests/unit/SettingsContext.test.tsx b/tests/unit/SettingsContext.test.tsx new file mode 100644 index 0000000..78b5672 --- /dev/null +++ b/tests/unit/SettingsContext.test.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { SettingsProvider, useSettings } from '../../src/components/SettingsContext'; + +const TestConsumer = ({ onRender }: { onRender?: (val: any) => void }) => { + const ctx = useSettings(); + if (onRender) onRender(ctx); + return ( +
+ {JSON.stringify(ctx.settings)} + + + +
+ ); +}; + +describe('SettingsContext', () => { + it('SettingsProvider renders its children', () => { + render( + +
child content
+
+ ); + expect(screen.getByTestId('child')).toBeInTheDocument(); + }); + + it('useSettings returns settings and updateSettings', () => { + let captured: any; + render( + + { captured = val; }} /> + + ); + expect(captured).toHaveProperty('settings'); + expect(captured).toHaveProperty('updateSettings'); + expect(typeof captured.updateSettings).toBe('function'); + }); + + it('initial settings is an empty object', () => { + render( + + + + ); + expect(screen.getByTestId('settings')).toHaveTextContent('{}'); + }); + + it('updateSettings merges new properties with existing settings', () => { + render( + + + + ); + fireEvent.click(screen.getByTestId('btn-a')); + const parsed = JSON.parse(screen.getByTestId('settings').textContent!); + expect(parsed.a).toBe('value-a'); + }); + + it('multiple updateSettings calls accumulate settings', () => { + render( + + + + ); + fireEvent.click(screen.getByTestId('btn-a')); + fireEvent.click(screen.getByTestId('btn-b')); + const parsed = JSON.parse(screen.getByTestId('settings').textContent!); + expect(parsed.a).toBe('value-a'); + expect(parsed.b).toBe('value-b'); + }); + + it('updateSettings overwrites existing key when set again', () => { + render( + + + + ); + fireEvent.click(screen.getByTestId('btn-a')); + fireEvent.click(screen.getByTestId('btn-c')); + const parsed = JSON.parse(screen.getByTestId('settings').textContent!); + expect(parsed.a).toBe('updated-a'); + }); +}); diff --git a/tests/unit/Sidebar/CustomDropDown.test.tsx b/tests/unit/Sidebar/CustomDropDown.test.tsx new file mode 100644 index 0000000..c66d94b --- /dev/null +++ b/tests/unit/Sidebar/CustomDropDown.test.tsx @@ -0,0 +1,154 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { renderWithIntl } from '../testUtils'; +import { CustomDropdown } from '../../../src/components/Sidebar/CustomDropDown'; + +const defaultOptions = [ + { label: 'Option A', value: 'option-a' }, + { label: 'Option B', value: 'option-b' }, + { label: 'Option C', value: 'option-c' }, +]; + +describe('CustomDropdown', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the placeholder text', () => { + renderWithIntl( + + ); + expect(screen.getByText('Select...')).toBeInTheDocument(); + }); + + it('renders options in the DOM', () => { + renderWithIntl( + + ); + expect(screen.getByText('Option A')).toBeInTheDocument(); + expect(screen.getByText('Option B')).toBeInTheDocument(); + expect(screen.getByText('Option C')).toBeInTheDocument(); + }); + + it('clicking an option calls onSelectionChange with its value', () => { + const onChange = jest.fn(); + renderWithIntl( + + ); + fireEvent.click(screen.getByText('Option B')); + expect(onChange).toHaveBeenCalledWith('option-b'); + }); + + it('when wholeButtonActionable=false, clicking placeholder calls onSelectionChange with first option value', () => { + const onChange = jest.fn(); + renderWithIntl( + + ); + fireEvent.click(screen.getByText('Select...')); + expect(onChange).toHaveBeenCalledWith('option-a'); + }); + + it('when wholeButtonActionable=true, clicking placeholder toggles the dropdown', () => { + const onChange = jest.fn(); + const { container } = renderWithIntl( + + ); + // Click placeholder - should toggle dropdown (open), not call onSelectionChange + fireEvent.click(screen.getByText('Select...')); + expect(onChange).not.toHaveBeenCalled(); + }); + + it('showDivider=false renders fewer spans than showDivider=true', () => { + // The divider is a element rendered conditionally + // When showDivider=true: placeholder span + divider span = 2 spans in selected area + // When showDivider=false: only placeholder span = 1 span in selected area + const { container: cTrue } = renderWithIntl( + + ); + const { container: cFalse } = renderWithIntl( + + ); + const spansTrue = cTrue.querySelector('[class*="dropdown-selected"]')?.querySelectorAll('span') ?? []; + const spansFalse = cFalse.querySelector('[class*="dropdown-selected"]')?.querySelectorAll('span') ?? []; + // With divider: more spans than without + // Note: CSS module class is undefined, so we find dropdown-selected by first div containing spans + // Alternative: count all spans in the whole dropdown + const totalTrue = cTrue.querySelectorAll('span').length; + const totalFalse = cFalse.querySelectorAll('span').length; + expect(totalTrue).toBeGreaterThan(totalFalse); + }); + + it('clicking outside the dropdown fires mousedown on document', () => { + const onChange = jest.fn(); + renderWithIntl( + + ); + // Open dropdown first + fireEvent.click(screen.getByText('Select...')); + // Click outside (on document body) + fireEvent.mouseDown(document.body); + // Dropdown should close - clicking an option now would still trigger onChange + // We just verify no error thrown + expect(onChange).not.toHaveBeenCalled(); + }); + + it('option ids are set correctly', () => { + renderWithIntl( + + ); + expect(document.getElementById('my-dropdown-0')).toBeTruthy(); + expect(document.getElementById('my-dropdown-1')).toBeTruthy(); + expect(document.getElementById('my-dropdown-2')).toBeTruthy(); + }); +}); diff --git a/tests/unit/Sidebar/Sidebar.test.tsx b/tests/unit/Sidebar/Sidebar.test.tsx new file mode 100644 index 0000000..2771065 --- /dev/null +++ b/tests/unit/Sidebar/Sidebar.test.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { screen, fireEvent } from '@testing-library/react'; +import { renderWithIntl } from '../testUtils'; +import { Sidebar } from '../../../src/components/Sidebar/Sidebar'; + +jest.mock('../../../src/functions/utility', () => ({ + sideBarCommand: jest.fn(), +})); + +const defaultProps = { + onItemSelect: jest.fn(), + selectedSidebarItem: 'Recent' as SidebarItem, +}; + +describe('Sidebar', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders "Dynamo" logo text', () => { + renderWithIntl(); + expect(screen.getByText('Dynamo')).toBeInTheDocument(); + }); + + it('renders Recent navigation item', () => { + renderWithIntl(); + expect(screen.getByText('Recent')).toBeInTheDocument(); + }); + + it('renders Samples navigation item', () => { + renderWithIntl(); + expect(screen.getByText('Samples')).toBeInTheDocument(); + }); + + it('renders Learning navigation item', () => { + renderWithIntl(); + expect(screen.getByText('Learning')).toBeInTheDocument(); + }); + + it('renders Discussion Forum external link', () => { + renderWithIntl(); + expect(screen.getByText('Discussion Forum')).toBeInTheDocument(); + }); + + it('renders Dynamo Website external link', () => { + renderWithIntl(); + expect(screen.getByText('Dynamo Website')).toBeInTheDocument(); + }); + + it('renders Dynamo Primer external link', () => { + renderWithIntl(); + expect(screen.getByText('Dynamo Primer')).toBeInTheDocument(); + }); + + it('renders Github Repository external link', () => { + renderWithIntl(); + expect(screen.getByText('Github Repository')).toBeInTheDocument(); + }); + + it('renders Send Issues external link', () => { + renderWithIntl(); + expect(screen.getByText('Send Issues')).toBeInTheDocument(); + }); + + it('clicking Recent calls onItemSelect with "Recent"', () => { + renderWithIntl(); + fireEvent.click(screen.getByText('Recent')); + expect(defaultProps.onItemSelect).toHaveBeenCalledWith('Recent'); + }); + + it('clicking Samples calls onItemSelect with "Samples"', () => { + renderWithIntl(); + fireEvent.click(screen.getByText('Samples')); + expect(defaultProps.onItemSelect).toHaveBeenCalledWith('Samples'); + }); + + it('clicking Learning calls onItemSelect with "Learning"', () => { + renderWithIntl(); + fireEvent.click(screen.getByText('Learning')); + expect(defaultProps.onItemSelect).toHaveBeenCalledWith('Learning'); + }); + + it('renders three clickable sidebar items', () => { + // Tests that all 3 sidebar items are rendered and clickable + const onItemSelect = jest.fn(); + renderWithIntl(); + ['Recent', 'Samples', 'Learning'].forEach(item => { + expect(screen.getByText(item)).toBeInTheDocument(); + }); + }); + + it('changing selectedSidebarItem to Samples does not crash', () => { + expect(() => + renderWithIntl() + ).not.toThrow(); + }); + + it('renders the Open dropdown', () => { + renderWithIntl(); + expect(screen.getByText('Open')).toBeInTheDocument(); + }); + + it('renders the New dropdown', () => { + renderWithIntl(); + expect(screen.getByText('New')).toBeInTheDocument(); + }); +}); diff --git a/tests/unit/localization.test.ts b/tests/unit/localization.test.ts new file mode 100644 index 0000000..de3fb64 --- /dev/null +++ b/tests/unit/localization.test.ts @@ -0,0 +1,110 @@ +import { getMessagesForLocale } from '../../src/localization/localization'; + +describe('getMessagesForLocale', () => { + it('returns English messages for "en"', () => { + const msgs = getMessagesForLocale('en'); + expect(msgs['title.text.recent']).toBe('Recent'); + }); + + it('returns English messages for "en-US"', () => { + const msgs = getMessagesForLocale('en-US'); + expect(msgs['title.text.recent']).toBe('Recent'); + }); + + it('returns Spanish messages for "es-ES"', () => { + const msgs = getMessagesForLocale('es-ES'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns German messages for "de-DE"', () => { + const msgs = getMessagesForLocale('de-DE'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns Czech messages for "cs-CZ"', () => { + const msgs = getMessagesForLocale('cs-CZ'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns French messages for "fr-FR"', () => { + const msgs = getMessagesForLocale('fr-FR'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns Italian messages for "it-IT"', () => { + const msgs = getMessagesForLocale('it-IT'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns Japanese messages for "ja-JP"', () => { + const msgs = getMessagesForLocale('ja-JP'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns Korean messages for "ko-KR"', () => { + const msgs = getMessagesForLocale('ko-KR'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns Polish messages for "pl-PL"', () => { + const msgs = getMessagesForLocale('pl-PL'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns Portuguese (Brazil) messages for "pt-BR"', () => { + const msgs = getMessagesForLocale('pt-BR'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns Russian messages for "ru-RU"', () => { + const msgs = getMessagesForLocale('ru-RU'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns Simplified Chinese messages for "zh-Hans"', () => { + const msgs = getMessagesForLocale('zh-Hans'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns Simplified Chinese messages for "zh-CN"', () => { + const msgs = getMessagesForLocale('zh-CN'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns Traditional Chinese messages for "zh-Hant"', () => { + const msgs = getMessagesForLocale('zh-Hant'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns Traditional Chinese messages for "zh-TW"', () => { + const msgs = getMessagesForLocale('zh-TW'); + expect(msgs).toBeDefined(); + expect(typeof msgs['title.text.recent']).toBe('string'); + }); + + it('returns English as fallback for unknown locale', () => { + const msgs = getMessagesForLocale('xx-XX' as Locale); + expect(msgs['title.text.recent']).toBe('Recent'); + }); + + it('zh-Hans and zh-CN return the same messages', () => { + expect(getMessagesForLocale('zh-Hans')).toBe(getMessagesForLocale('zh-CN')); + }); + + it('zh-Hant and zh-TW return the same messages', () => { + expect(getMessagesForLocale('zh-Hant')).toBe(getMessagesForLocale('zh-TW')); + }); +}); diff --git a/tests/unit/testUtils.tsx b/tests/unit/testUtils.tsx new file mode 100644 index 0000000..455a66c --- /dev/null +++ b/tests/unit/testUtils.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import { SettingsProvider } from '../../src/components/SettingsContext'; +import { getMessagesForLocale } from '../../src/localization/localization'; + +const enMessages = getMessagesForLocale('en'); + +export const renderWithIntl = (ui: React.ReactElement, locale: Locale = 'en') => { + const messages = getMessagesForLocale(locale); + return render( + + {ui} + + ); +}; + +export const renderWithProviders = (ui: React.ReactElement) => { + return render( + + + {ui} + + + ); +}; diff --git a/tests/unit/utility.test.ts b/tests/unit/utility.test.ts new file mode 100644 index 0000000..b63997f --- /dev/null +++ b/tests/unit/utility.test.ts @@ -0,0 +1,106 @@ +import { openFile, startGuidedTour, sideBarCommand, showSamplesCommand, saveHomePageSettings } from '../../src/functions/utility'; + +describe('utility functions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('openFile', () => { + it('calls scriptObject.OpenFile when chrome.webview exists', () => { + openFile('/some/path/file.dyn'); + expect(window.chrome.webview.hostObjects.scriptObject.OpenFile).toHaveBeenCalledWith('/some/path/file.dyn'); + }); + + it('does not throw when chrome is undefined', () => { + const savedChrome = window.chrome; + (window as any).chrome = undefined; + expect(() => openFile('/path')).not.toThrow(); + window.chrome = savedChrome; + }); + }); + + describe('startGuidedTour', () => { + it('calls scriptObject.StartGuidedTour when chrome.webview exists', () => { + startGuidedTour('geometry'); + expect(window.chrome.webview.hostObjects.scriptObject.StartGuidedTour).toHaveBeenCalledWith('geometry'); + }); + + it('does not throw when chrome is undefined', () => { + const savedChrome = window.chrome; + (window as any).chrome = undefined; + expect(() => startGuidedTour('geometry')).not.toThrow(); + window.chrome = savedChrome; + }); + }); + + describe('sideBarCommand', () => { + it('"open-file" calls OpenWorkspace', () => { + sideBarCommand('open-file'); + expect(window.chrome.webview.hostObjects.scriptObject.OpenWorkspace).toHaveBeenCalled(); + }); + + it('"open-template" calls ShowTempalte', () => { + sideBarCommand('open-template'); + expect(window.chrome.webview.hostObjects.scriptObject.ShowTempalte).toHaveBeenCalled(); + }); + + it('"open-backup-locations" calls ShowBackupFilesInFolder', () => { + sideBarCommand('open-backup-locations'); + expect(window.chrome.webview.hostObjects.scriptObject.ShowBackupFilesInFolder).toHaveBeenCalled(); + }); + + it('"workspace" calls NewWorkspace', () => { + sideBarCommand('workspace'); + expect(window.chrome.webview.hostObjects.scriptObject.NewWorkspace).toHaveBeenCalled(); + }); + + it('"custom-node" calls NewCustomNodeWorkspace', () => { + sideBarCommand('custom-node'); + expect(window.chrome.webview.hostObjects.scriptObject.NewCustomNodeWorkspace).toHaveBeenCalled(); + }); + + it('does not call any function when chrome is undefined', () => { + const savedChrome = window.chrome; + (window as any).chrome = undefined; + expect(() => sideBarCommand('open-file')).not.toThrow(); + window.chrome = savedChrome; + expect(window.chrome.webview.hostObjects.scriptObject.OpenWorkspace).not.toHaveBeenCalled(); + }); + }); + + describe('showSamplesCommand', () => { + it('"open-graphs" calls ShowSampleFilesInFolder', () => { + showSamplesCommand('open-graphs'); + expect(window.chrome.webview.hostObjects.scriptObject.ShowSampleFilesInFolder).toHaveBeenCalled(); + }); + + it('"open-datasets" calls ShowSampleDatasetsInFolder', () => { + showSamplesCommand('open-datasets'); + expect(window.chrome.webview.hostObjects.scriptObject.ShowSampleDatasetsInFolder).toHaveBeenCalled(); + }); + + it('does not throw when chrome is undefined', () => { + const savedChrome = window.chrome; + (window as any).chrome = undefined; + expect(() => showSamplesCommand('open-graphs')).not.toThrow(); + window.chrome = savedChrome; + }); + }); + + describe('saveHomePageSettings', () => { + it('calls SaveSettings with JSON.stringify of settings object when chrome.webview exists', () => { + const settings = { recentPageViewMode: 'grid', sideBarWidth: '300' }; + saveHomePageSettings(settings); + expect(window.chrome.webview.hostObjects.scriptObject.SaveSettings).toHaveBeenCalledWith( + JSON.stringify(settings) + ); + }); + + it('does not throw when chrome is undefined', () => { + const savedChrome = window.chrome; + (window as any).chrome = undefined; + expect(() => saveHomePageSettings({ a: 1 })).not.toThrow(); + window.chrome = savedChrome; + }); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index f1fa9c9..b2e44df 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,3 +1,5 @@ +/// + declare module 'react-split-pane'; declare module 'react-table'; From dbb99e7ff80e17f32f5c0a158a1bf56cbb24e909 Mon Sep 17 00:00:00 2001 From: Daniel Velazco Date: Thu, 26 Mar 2026 16:33:21 -0500 Subject: [PATCH 02/12] refine agents and skills, add e2e tests --- .claude/agents.md | 21 - .claude/agents/testing-agent.md | 77 +--- .claude/knowledge/architecture.md | 89 ---- .claude/knowledge/domain-dynamo.md | 86 ---- .claude/knowledge/project-conventions.md | 92 ---- .claude/knowledge/stack.md | 78 ---- .claude/settings.local.json | 5 +- .claude/skills/bugfix/SKILL.md | 64 +++ .../SKILL.md} | 0 .../{code-review.md => code-review/SKILL.md} | 47 +- .../SKILL.md} | 5 +- .claude/skills/feature-ui/SKILL.md | 91 ++++ .../SKILL.md} | 0 .claude/skills/{react.md => react/SKILL.md} | 0 .claude/skills/refactor/SKILL.md | 62 +++ .../SKILL.md} | 0 .claude/workflows/bugfix.md | 68 --- .claude/workflows/feature-ui.md | 90 ---- .claude/workflows/pr-review.md | 78 ---- .claude/workflows/refactor.md | 64 --- .../page-2026-03-26T16-45-43-389Z.yml | 69 +++ .../page-2026-03-26T16-45-54-366Z.yml | 69 +++ .../page-2026-03-26T16-46-10-498Z.yml | 415 ++++++++++++++++++ .../page-2026-03-26T16-46-26-627Z.yml | 281 ++++++++++++ .../page-2026-03-26T16-46-42-925Z.yml | 71 +++ .../page-2026-03-26T16-46-44-464Z.yml | 71 +++ .../page-2026-03-26T16-46-53-977Z.yml | 126 ++++++ .../page-2026-03-26T16-46-55-714Z.yml | 126 ++++++ .../page-2026-03-26T16-47-08-146Z.yml | 124 ++++++ .../page-2026-03-26T16-47-09-786Z.yml | 124 ++++++ .../page-2026-03-26T16-47-23-427Z.yml | 129 ++++++ .../page-2026-03-26T16-47-25-533Z.yml | 129 ++++++ .../page-2026-03-26T16-47-43-906Z.yml | 129 ++++++ .../page-2026-03-26T16-48-00-025Z.yml | 129 ++++++ .../page-2026-03-26T16-48-09-485Z.yml | 124 ++++++ .../page-2026-03-26T16-48-11-041Z.yml | 124 ++++++ .../page-2026-03-26T16-48-18-913Z.yml | 128 ++++++ .../page-2026-03-26T16-48-20-477Z.yml | 128 ++++++ CLAUDE.md | 151 +++++++ package.json | 2 +- playwright.config.js | 1 + src/components/CLAUDE.md | 9 + src/components/Common/CardItem.tsx | 2 +- src/components/Learning/Carousel.tsx | 4 +- src/components/Learning/PageLearning.tsx | 12 +- src/components/Recent/PageRecent.tsx | 16 +- src/components/Samples/PageSamples.tsx | 18 +- src/components/Samples/SamplesGrid.tsx | 2 +- src/components/Sidebar/CustomDropDown.tsx | 10 +- src/components/Sidebar/Sidebar.tsx | 8 +- src/locales/CLAUDE.md | 10 + tests/CLAUDE.md | 16 + tests/e2e/components/Sidebar.ts | 29 ++ tests/e2e/e2e.test.ts | 12 - tests/e2e/learning.spec.ts | 72 +++ tests/e2e/navigation.spec.ts | 56 +++ tests/e2e/pages/LearningPage.ts | 32 ++ tests/e2e/pages/RecentPage.ts | 28 ++ tests/e2e/pages/SamplesPage.ts | 26 ++ tests/e2e/recent.spec.ts | 65 +++ tests/e2e/samples.spec.ts | 56 +++ tests/e2e/sidebar.spec.ts | 43 ++ 62 files changed, 3402 insertions(+), 791 deletions(-) delete mode 100644 .claude/agents.md delete mode 100644 .claude/knowledge/architecture.md delete mode 100644 .claude/knowledge/domain-dynamo.md delete mode 100644 .claude/knowledge/project-conventions.md delete mode 100644 .claude/knowledge/stack.md create mode 100644 .claude/skills/bugfix/SKILL.md rename .claude/skills/{build-tooling.md => build-tooling/SKILL.md} (100%) rename .claude/skills/{code-review.md => code-review/SKILL.md} (62%) rename .claude/skills/{playwright.md => end-to-end-testing/SKILL.md} (97%) create mode 100644 .claude/skills/feature-ui/SKILL.md rename .claude/skills/{localization.md => localization/SKILL.md} (100%) rename .claude/skills/{react.md => react/SKILL.md} (100%) create mode 100644 .claude/skills/refactor/SKILL.md rename .claude/skills/{unit-testing.md => unit-testing/SKILL.md} (100%) delete mode 100644 .claude/workflows/bugfix.md delete mode 100644 .claude/workflows/feature-ui.md delete mode 100644 .claude/workflows/pr-review.md delete mode 100644 .claude/workflows/refactor.md create mode 100644 .playwright-cli/page-2026-03-26T16-45-43-389Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-45-54-366Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-46-10-498Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-46-26-627Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-46-42-925Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-46-44-464Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-46-53-977Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-46-55-714Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-47-08-146Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-47-09-786Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-47-23-427Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-47-25-533Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-47-43-906Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-48-00-025Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-48-09-485Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-48-11-041Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-48-18-913Z.yml create mode 100644 .playwright-cli/page-2026-03-26T16-48-20-477Z.yml create mode 100644 CLAUDE.md create mode 100644 src/components/CLAUDE.md create mode 100644 src/locales/CLAUDE.md create mode 100644 tests/CLAUDE.md create mode 100644 tests/e2e/components/Sidebar.ts delete mode 100644 tests/e2e/e2e.test.ts create mode 100644 tests/e2e/learning.spec.ts create mode 100644 tests/e2e/navigation.spec.ts create mode 100644 tests/e2e/pages/LearningPage.ts create mode 100644 tests/e2e/pages/RecentPage.ts create mode 100644 tests/e2e/pages/SamplesPage.ts create mode 100644 tests/e2e/recent.spec.ts create mode 100644 tests/e2e/samples.spec.ts create mode 100644 tests/e2e/sidebar.spec.ts diff --git a/.claude/agents.md b/.claude/agents.md deleted file mode 100644 index 00b8b80..0000000 --- a/.claude/agents.md +++ /dev/null @@ -1,21 +0,0 @@ -# Claude Agents – DynamoHome - -Agent definitions have moved to individual files in `.claude/agents/`. Each file uses YAML frontmatter with `name`, `description`, `model`, and `tools`, followed by a detailed system prompt. - -## Available agents - -| Agent | File | Use when | -|---|---|---| -| **frontend-agent** | `agents/frontend-agent.md` | Implementing/modifying React components, UI features, localization, unit tests | -| **testing-agent** | `agents/testing-agent.md` | Writing or maintaining Playwright e2e tests, building Page Object classes | -| **build-agent** | `agents/build-agent.md` | Modifying webpack config, tsconfig, jest config, npm scripts, CI pipelines | -| **code-review-agent** | `agents/code-review-agent.md` | Reviewing PRs for correctness, test coverage, localization, Dynamo integration | - -## Agent selection guide - -- **New React component or UI change** → `frontend-agent` -- **Bug in a component's rendering or interaction** → `frontend-agent` -- **Missing or broken unit tests** → `frontend-agent` -- **New e2e test scenario or Playwright POM class** → `testing-agent` -- **Build failure, webpack change, tsconfig change** → `build-agent` -- **PR review or code quality assessment** → `code-review-agent` diff --git a/.claude/agents/testing-agent.md b/.claude/agents/testing-agent.md index b8fe6b5..b047893 100644 --- a/.claude/agents/testing-agent.md +++ b/.claude/agents/testing-agent.md @@ -13,6 +13,8 @@ tools: You are an End-to-End Test Engineer working on DynamoHome, a React 18 SPA running inside a Chrome WebView in the Dynamo desktop application. You own all Playwright e2e tests. +Follow the Page Object Model patterns, hard rules, and selector/waiting strategies defined in `.claude/skills/end-to-end-testing.md`. + ## Test infrastructure ``` @@ -32,78 +34,6 @@ tests/ playwright.config.js # Config: testDir=./tests/e2e, Chromium only, port 8080, 30s timeout, 2 retries on CI ``` -Run e2e tests: `npm run test:e2e` -Start dev server first if running locally: `npm run start` (serves on `http://localhost:8080`) - -## Page Object Model — mandatory structure - -Every test must follow strict POM. Create files alongside tests in `tests/`: - -``` -tests/ - e2e/ - e2e.test.ts # Orchestration only — no selectors or page actions here - pages/ - RecentPage.ts # Represents the Recent files page - SamplesPage.ts # Represents the Samples page - LearningPage.ts # Represents the Learning page - components/ - Sidebar.ts # Sidebar navigation component class - CardItem.ts # Shared card grid item - GraphTable.ts # Table component (Recent/Samples) - Carousel.ts # Carousel component (Learning) - unit/ - App.test.tsx # Unit tests (Jest) - jest.setup.ts # Applied globally via jest.config.ts - __mocks__/ # Auto-applied mocks -``` - -### Page class pattern - -```typescript -import { Page } from '@playwright/test'; - -export class RecentPage { - constructor(private page: Page) {} - - // All selectors defined here — NEVER in test files - private graphGridItems = () => this.page.locator('[data-testid="graph-grid-item"]'); - private listViewToggle = () => this.page.locator('[data-testid="list-view-toggle"]'); - - // All actions defined here — NEVER in test files - async switchToListView() { - await this.listViewToggle().click(); - } - - async getGraphCount(): Promise { - return this.graphGridItems().count(); - } -} -``` - -### Test file pattern - -```typescript -import { test, expect } from '@playwright/test'; -import { RecentPage } from './pages/RecentPage'; - -test('recent page displays graphs in grid view by default', async ({ page }) => { - const recentPage = new RecentPage(page); - await page.goto('http://localhost:8080'); - const count = await recentPage.getGraphCount(); - expect(count).toBeGreaterThan(0); -}); -``` - -## Hard rules - -- **Test files must not contain selectors** (no `.locator()`, no `page.$()`, no `data-testid` strings in test files) -- **Test files must not contain direct Playwright actions** (no `await page.click()`, `await page.fill()` in test files) -- **All selectors live in Page or Component classes** -- **All actions live in Page or Component classes** -- Prefer `data-testid` attributes for selectors; if missing, add them to the component via the frontend-agent -- Tests must be deterministic — avoid `waitForTimeout()`; use `waitForSelector()` or expect-based waiting instead - ## Application pages to cover The app has three main pages switchable via the Sidebar: @@ -122,8 +52,7 @@ The app uses `window.chrome.webview.hostObjects.scriptObject` for all backend ca ```bash npm run test:e2e # Run all Playwright tests npm run start # Start dev server (required before running e2e locally) -npx playwright show-report # View HTML test report after run -playwright-cli open http://localhost:8080 # Exploratory testing / selector discovery +npx playwright show-report # View HTML test report after run ``` ## What NOT to do diff --git a/.claude/knowledge/architecture.md b/.claude/knowledge/architecture.md deleted file mode 100644 index e464681..0000000 --- a/.claude/knowledge/architecture.md +++ /dev/null @@ -1,89 +0,0 @@ -# Architecture – DynamoHome - -DynamoHome is a **React 18 single-page application** that serves as the home/start page for Dynamo, an Autodesk visual programming tool. It runs inside a **Chrome WebView** embedded in the Dynamo desktop application — not in a standalone browser. - -## Component tree - -``` -App.tsx # IntlProvider (localization) + SettingsProvider (context) -└── LayoutContainer.tsx # SplitPane: resizable sidebar + main content - ├── Sidebar.tsx # Left nav panel — page switching + custom dropdowns - │ └── CustomDropDown.tsx - └── MainContent.tsx # Renders active page based on sidebar selection - ├── PageRecent.tsx # Recent Dynamo files — grid or table view - │ ├── GraphGridItem.tsx - │ └── GraphTable.tsx # react-table with custom cell renderers - │ ├── CustomNameCellRenderer.tsx - │ ├── CustomLocationCellRenderer.tsx - │ └── CustomAuthorCellRenderer.tsx - ├── PageSamples.tsx # Sample graphs — grid or table view - │ ├── SamplesGrid.tsx - │ │ └── SamplesGridItem.tsx - │ └── SamplesTable.tsx - │ └── CustomSampleFirstCellRenderer.tsx - └── PageLearning.tsx # Learning resources — guides + video carousels - ├── Carousel.tsx - ├── GuideGridItem.tsx - ├── ModalItem.tsx - └── VideoCarouselItem.tsx - -Common/ # Shared across modules - CardItem.tsx # Reusable card for grid views - Tooltip.tsx - Arrow.tsx - Portal.tsx - CustomIcons.tsx # SVG icons: GridView, ListView -``` - -## Data flow - -``` -Dynamo backend (.NET) - │ - ▼ calls window globals -window.receiveGraphDataFromDotNet(json) → PageRecent state -window.receiveSamplesDataFromDotNet(json) → PageSamples state -window.receiveTrainingVideoDataFromDotNet(json) → PageLearning state -window.receiveInteractiveGuidesDataFromDotNet(json) → PageLearning state - │ - ▼ -Component setState → React render - │ - ▼ user interaction -window.chrome.webview.hostObjects.scriptObject.OpenFile(path) -window.chrome.webview.hostObjects.scriptObject.DeleteFile(path) -window.chrome.webview.hostObjects.scriptObject.OpenUrl(url) - │ - ▼ -Back to Dynamo -``` - -In **development mode** (no WebView), the app detects `!window.chrome?.webview` and loads mock data from `src/assets/home.ts`, `samples.ts`, `learning.ts`. - -## Settings persistence - -`SettingsContext.tsx` holds: `recentPageViewMode`, `samplesViewMode`, `sideBarWidth` - -- Loaded on init via `window.chrome.webview.hostObjects.scriptObject.GetHomePageSettings()` -- Saved on change via `saveHomePageSettings()` in `src/functions/utility.ts` -- Consumed via the `useSettings()` custom hook - -## Localization - -- `App.tsx` wraps the tree with `` -- Locale is set at runtime by Dynamo calling `window.setLocale(locale)` -- Messages come from `src/localization/localization.ts → getMessagesForLocale(locale)` -- 14 locales: en, cs, de, es, fr, it, ja, ko, pl, pt-BR, ru, zh-Hans, zh-Hant - -## Build output - -- **Entry**: `src/index.tsx` -- **Output**: `dist/build/index.bundle.js` + `dist/build/index.html` -- Dynamo hardcodes the path `dist/build/index.bundle.js` — do not change it - -## Key constraints - -- No backend server — all data comes from the Dynamo host via WebView -- No routing library — page switching is managed by component state in `MainContent.tsx` -- No Redux or external state management — React Context + useState only -- The app must work in Chromium (WebView) — no reliance on Firefox/Safari-specific APIs diff --git a/.claude/knowledge/domain-dynamo.md b/.claude/knowledge/domain-dynamo.md deleted file mode 100644 index 6e24f6d..0000000 --- a/.claude/knowledge/domain-dynamo.md +++ /dev/null @@ -1,86 +0,0 @@ -# Dynamo Domain Knowledge - -## What Dynamo is - -Dynamo is a visual programming tool by Autodesk used in architecture, engineering, and construction. DynamoHome is its start page — a webview-based UI embedded in the Dynamo desktop application. - -## How the host integration works - -The app communicates with the Dynamo host (.NET) exclusively through `window.chrome.webview.hostObjects.scriptObject`. This object is injected by the WebView host and exposes backend methods. - -### Methods the app calls on Dynamo - -```typescript -scriptObject.OpenFile(filePath: string) // Open a Dynamo graph file -scriptObject.DeleteFile(filePath: string) // Delete a recent file entry -scriptObject.OpenUrl(url: string) // Open URL in browser -scriptObject.GetHomePageSettings(): string // Returns JSON settings string -scriptObject.SaveHomePageSettings(json: string) // Persist user preferences -scriptObject.PinGraph(filePath: string) // Pin/unpin recent file -``` - -All of these are async (return promises) and must be called with `await`. - -### Global callbacks Dynamo calls on the app - -These are set on `window` and called by the .NET host: - -```typescript -window.receiveGraphDataFromDotNet(json: string) // Recent files data -window.receiveSamplesDataFromDotNet(json: string) // Sample graphs data -window.receiveTrainingVideoDataFromDotNet(json: string) // Learning videos data -window.receiveInteractiveGuidesDataFromDotNet(json: string) // Learning guides data -window.setLocale(locale: string) // Change UI language -window.setShowStartPageChanged(show: boolean) // Loading overlay control -window.setHomePageSettings(settingsJson: string) // Apply persisted settings -``` - -**Never rename or remove these globals** — Dynamo calls them by name from .NET code. Any signature change is a breaking change. - -## Data shapes - -Recent file entry (from `receiveGraphDataFromDotNet`): -```json -{ - "Name": "MyGraph.dyn", - "Path": "C:\\Users\\user\\Documents\\MyGraph.dyn", - "Author": "user@company.com", - "TimeStamp": "2024-01-15T10:30:00", - "IsPinned": false -} -``` - -Sample graph entry: -```json -{ - "Name": "Sample Graph", - "Description": "A sample Dynamo graph", - "Path": "C:\\Program Files\\Dynamo\\samples\\...", - "ImagePath": "relative/path/to/image.png" -} -``` - -Settings JSON (persisted to Dynamo): -```json -{ - "recentPageViewMode": "grid", - "samplesViewMode": "list", - "sideBarWidth": 200 -} -``` - -## Development mode - -When `window.chrome?.webview` is not present (running via `npm start` outside Dynamo), the app uses mock data from: -- `src/assets/home.ts` — mock recent files -- `src/assets/samples.ts` — mock samples -- `src/assets/learning.ts` — mock learning content - -This allows full development without Dynamo installed. - -## Compatibility constraints - -- The output bundle path `dist/build/index.bundle.js` is hardcoded in Dynamo — never change it -- The app must run in Chromium (Edge WebView2) — no reliance on browser-specific APIs not in Chromium -- Locale identifiers must match what Dynamo sends (e.g., `"en-US"`, `"de-DE"`, `"zh-Hans"`) — check `src/localization/localization.ts` for the full mapping -- Settings schema is shared with Dynamo — adding new fields is safe; renaming/removing existing fields is a breaking change diff --git a/.claude/knowledge/project-conventions.md b/.claude/knowledge/project-conventions.md deleted file mode 100644 index caf3099..0000000 --- a/.claude/knowledge/project-conventions.md +++ /dev/null @@ -1,92 +0,0 @@ -# Project Conventions – DynamoHome - -## File and folder structure - -- Components live in `src/components/[Module]/ComponentName.tsx` -- Styles live in `src/components/[Module]/ComponentName.module.css` (CSS Modules, same folder as component) -- Shared components (used by 2+ modules) go in `src/components/Common/` -- Unit tests live in `tests/ComponentName.test.tsx` (flat, not mirrored structure) -- E2E page objects live in `tests/pages/` and `tests/components/` -- Locale strings live in `src/locales/[locale].json` — all 14 files must stay in sync - -## Naming conventions - -- **Components**: PascalCase (`GraphGridItem.tsx`, `CustomDropDown.tsx`) -- **CSS Module files**: same name as component (`GraphGridItem.module.css`) -- **Hooks**: camelCase prefixed with `use` (`useSettings`) -- **Type interfaces**: PascalCase, descriptive (`SidebarItem`, `HomePageSetting`) -- **Locale keys**: dot-notation, `module.element.descriptor` (`recent.table.header.name`, `button.title.text.open`) -- **Test files**: `ComponentName.test.tsx` for unit, `e2e.test.ts` for e2e - -## Component conventions - -```tsx -// ✅ Correct: explicit prop interface, CSS Modules, FormattedMessage -interface CardItemProps { - imageSrc: string; - onClick: () => void; - titleText: string; -} - -export const CardItem = ({ imageSrc, onClick, titleText }: CardItemProps) => { - return ( -
- -
- ); -}; - -// ❌ Wrong: inline types, hardcoded text, any type -export const CardItem = ({ imageSrc, onClick }: { imageSrc: any; onClick: any }) => ( -
Recent Files
-); -``` - -## CSS Modules convention - -```tsx -import styles from './CardItem.module.css'; - -// Access with bracket notation (kebab-case class names) -
-``` - -## Localization convention - -```tsx -// In JSX — use FormattedMessage - - -// In props or attributes — use useIntl hook -const intl = useIntl(); - -``` - -## Backend integration convention - -All calls to Dynamo backend go through `src/functions/utility.ts`: -```tsx -// ✅ Correct: use utility functions -import { openFile, saveHomePageSettings } from '../functions/utility'; - -// ❌ Wrong: inline webview calls in components -window.chrome.webview.hostObjects.scriptObject.OpenFile(path); -``` - -Always guard WebView access: -```tsx -// ✅ Correct -if (window.chrome?.webview) { - await scriptObject.OpenFile(path); -} else { - console.log('[DEV] OpenFile:', path); // dev fallback -} -``` - -## Scope discipline - -- Make the **smallest change** that satisfies the requirement -- Do not refactor adjacent code while implementing a feature -- Do not add dependencies without explicit user approval -- Do not change build config, test config, or CI pipelines as a side effect -- If you notice a bug or improvement opportunity outside your task scope, mention it — don't fix it unilaterally diff --git a/.claude/knowledge/stack.md b/.claude/knowledge/stack.md deleted file mode 100644 index 1de6e8d..0000000 --- a/.claude/knowledge/stack.md +++ /dev/null @@ -1,78 +0,0 @@ -# Technology Stack – DynamoHome - -## Core - -| Technology | Version | Purpose | -|---|---|---| -| React | ^18.2.0 | UI framework | -| React DOM | ^18.2.0 | DOM rendering via `createRoot` | -| TypeScript | ^5.4.5 | Type safety | - -## Build - -| Technology | Version | Purpose | -|---|---|---| -| Webpack | ^5.92.0 | Module bundler | -| Webpack CLI | ^5.1.4 | CLI runner | -| Webpack Dev Server | ^5.2.2 | Dev server, port 8080, hot reload | -| ts-loader | ^9.5.1 | TypeScript → Webpack | -| babel-loader | ^9.1.3 | JS/JSX → Webpack | -| Babel Core | ^7.23.5 | JS transpilation | -| css-loader | ^6.8.1 | CSS imports | -| style-loader | ^3.3.3 | Injects CSS into DOM | -| HtmlWebpackPlugin | ^5.5.4 | Generates index.html | -| TerserPlugin | bundled | Production minification | - -## UI Libraries - -| Library | Version | Purpose | -|---|---|---| -| react-intl | ^6.6.1 | Localization (FormattedMessage, useIntl) | -| react-split-pane | ^0.1.92 | Resizable sidebar/content split | -| react-table | ^7.8.0 | Headless table (Recent, Samples views) | - -## Testing - -| Technology | Version | Purpose | -|---|---|---| -| Jest | ^29.7.0 | Unit test runner | -| ts-jest | ^29.1.5 | TypeScript support in Jest | -| @testing-library/react | ^15.0.6 | Component testing utilities | -| @testing-library/dom | ^10.3.0 | DOM testing utilities | -| jest-environment-jsdom | ^29.7.0 | DOM simulation for unit tests | -| identity-obj-proxy | ^3.0.0 | CSS Modules mock in Jest | -| @types/jest | ^29.5.12 | Jest type definitions | -| Playwright | ^1.27.1 | E2E test runner | -| @playwright/test | ^1.27.1 | Playwright test framework | - -## Code Quality - -| Technology | Version | Purpose | -|---|---|---| -| ESLint | ^8.57.0 | Linting | -| eslint-plugin-react | ^7.34.1 | React-specific lint rules | - -## Key npm scripts - -```bash -npm run start # Dev server (webpack-dev-server, port 8080) -npm run build # Dev bundle (unminified) -npm run bundle # Production bundle (minified) -npm run production # bundle + copy metadata to dist/ -npm run test:unit # Jest unit tests -npm run test:e2e # Playwright e2e tests -npm run test # test:unit + test:e2e -npm run lint:check # ESLint check (read-only) -npm run lint:fix # ESLint auto-fix -``` - -## TypeScript config notes - -- `strict: false` — intentional; do not enable without user approval -- `target: "ES2016"`, `module: "CommonJS"`, `jsx: "react-jsx"` -- `resolveJsonModule: true` — needed for locale JSON imports -- Custom type roots: `./types/index.d.ts` for global declarations (Window extensions, Locale type) - -## Supported locales - -en, cs, de, es, fr, it, ja, ko, pl, pt-BR, ru, zh-Hans, zh-Hant (14 total) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a06d4a3..666327c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,10 @@ "permissions": { "allow": [ "Bash(find /c/repos/DynamoHome/.claude -type f -name *.md -o -name *.json -o -name *.yaml -o -name *.yml)", - "Bash(npx jest:*)" + "Bash(npx jest:*)", + "Skill(playwright-cli)", + "Bash(find C:/repos/DynamoHome/tests/e2e -type f -name *.ts -o -name *.tsx)", + "Bash(npm run:*)" ] } } diff --git a/.claude/skills/bugfix/SKILL.md b/.claude/skills/bugfix/SKILL.md new file mode 100644 index 0000000..007b7b9 --- /dev/null +++ b/.claude/skills/bugfix/SKILL.md @@ -0,0 +1,64 @@ +# Bug Fix Workflow – DynamoHome + +Use this workflow when diagnosing and fixing a bug. + +## Steps + +### 1. Reproduce the issue + +Before touching any code: +- Identify the affected module: Recent, Samples, Learning, Sidebar, or layout +- Check whether the bug occurs in dev mode (`npm run start`) or only inside Dynamo (WebView context) +- Dev-mode bugs: reproduce with mock data from `src/assets/` +- WebView-only bugs: root cause is likely in `src/functions/utility.ts` or a missing `window.chrome?.webview` guard + +### 2. Locate the root cause + +Common bug locations in DynamoHome: + +| Symptom | Where to look | +|---|---| +| Data not displaying | `receiveXxxDataFromDotNet` callback and JSON parsing in the page component | +| Localization broken / missing text | Key missing in one or more of the 14 locale files | +| Settings not persisting | `saveHomePageSettings()` in `utility.ts` and `SettingsContext.tsx` | +| Crash on load / outside Dynamo | Missing `window.chrome?.webview` optional chaining guard | +| Wrong view mode shown | `SettingsContext` values (`recentPageViewMode`, `samplesViewMode`) | +| Action does nothing in Dynamo | Missing `await` on `scriptObject.*` calls — all hostObjects methods are async | + +### 3. Apply the fix + +- Make the **smallest change** that fixes the issue — do not refactor adjacent code +- Do not change unrelated files +- If the fix touches `window` globals or the settings JSON schema: + - Global callback names must not change + - Settings field names must not change (adding new fields is safe) + +### 4. Add a regression test + +For every bug fixed, add or update a test that would have caught it: + +```tsx +// tests/unit/ComponentName.test.tsx +it('does not crash when graphData is empty', () => { + render(); + expect(screen.getByText(/no recent files/i)).toBeInTheDocument(); +}); +``` + +Run: `npm run test:unit` + +### 5. Verify everything passes + +```bash +npm run lint:check # No new lint errors +npm run test:unit # All unit tests pass (including new regression test) +npm run build # Dev bundle still builds +``` + +For bugs involving a full user flow, also run: `npm run test:e2e` + +## Dynamo-specific notes + +- All `scriptObject.*` methods are async — missing `await` is a common source of silent failures +- `GetHomePageSettings()` JSON parsing is wrapped in try-catch in `LayoutContainer.tsx` — check that logic for settings-related bugs +- If a bug only reproduces inside Dynamo (not in dev mode), trace the call through `src/functions/utility.ts` — that is the only place WebView calls should be made diff --git a/.claude/skills/build-tooling.md b/.claude/skills/build-tooling/SKILL.md similarity index 100% rename from .claude/skills/build-tooling.md rename to .claude/skills/build-tooling/SKILL.md diff --git a/.claude/skills/code-review.md b/.claude/skills/code-review/SKILL.md similarity index 62% rename from .claude/skills/code-review.md rename to .claude/skills/code-review/SKILL.md index 2bb0f2f..a058e78 100644 --- a/.claude/skills/code-review.md +++ b/.claude/skills/code-review/SKILL.md @@ -4,6 +4,39 @@ Focus on real problems: regressions, missing tests, broken localization, type safety gaps, Dynamo integration breakage. Do not request style changes or refactors for code not touched by the PR. Keep feedback concise and actionable. +## Review process + +### 1. Understand the scope + +Read the PR description and list of changed files. Identify: +- Which module(s) are touched: Recent, Samples, Learning, Sidebar, Common, build, tests +- Whether it is a feature, bugfix, refactor, or dependency update +- Any Dynamo integration touchpoints (window globals, settings schema, output path) + +### 2. Work through the checklist + +See checklist below. Flag blockers as you go; note recommendations separately. + +### 3. Verify test coverage + +- New/modified components have unit tests in `tests/unit/` +- Changed user flows have Playwright coverage +- `tests/e2e/e2e.test.ts` contains no selectors or direct page actions + +### 4. Check build integrity + +Confirm (or ask the author to confirm): +```bash +npm run lint:check # Passes +npm run test:unit # Passes +npm run build # Dev bundle succeeds +npm run bundle # Production bundle succeeds (only for PRs touching webpack or build scripts) +``` + +### 5. Write structured feedback + +See "How to give feedback" section below. + ## Checklist ### TypeScript @@ -25,7 +58,7 @@ Focus on real problems: regressions, missing tests, broken localization, type sa - [ ] No hardcoded text in JSX (no raw string children, no `title=""` without intl) ### Unit tests -- [ ] New or modified components have test files in `tests/` +- [ ] New or modified components have test files in `tests/unit/` - [ ] Tests validate behavior (user sees X, clicking Y does Z), not implementation details - [ ] No re-mocking of globals already provided by `tests/__mocks__/chromeMock.ts` - [ ] Coverage not reduced for modified files @@ -47,6 +80,18 @@ Focus on real problems: regressions, missing tests, broken localization, type sa - [ ] `npm run build` produces a valid bundle - [ ] Existing npm script names unchanged +## Common blockers + +| Issue | Why it blocks | +|---|---| +| Missing locale key in any of the 14 locale files | App throws at runtime for users of that locale | +| `window.chrome.webview` accessed without optional chaining | Crashes in dev mode / outside Dynamo | +| Global callback renamed or removed | Dynamo .NET host calls it by name — breaking change | +| Settings field renamed or removed | Dynamo reads/writes settings by field name — breaking change | +| Selector in `e2e.test.ts` directly | Violates POM requirement; makes tests brittle | +| New npm dependency without justification | Increases bundle size, adds supply chain risk | +| Output path changed from `dist/build/` | Dynamo integration breaks at runtime | + ## How to give feedback **Blocker** (must fix before merge): diff --git a/.claude/skills/playwright.md b/.claude/skills/end-to-end-testing/SKILL.md similarity index 97% rename from .claude/skills/playwright.md rename to .claude/skills/end-to-end-testing/SKILL.md index ca9ed9c..37e628d 100644 --- a/.claude/skills/playwright.md +++ b/.claude/skills/end-to-end-testing/SKILL.md @@ -1,6 +1,6 @@ -# Playwright Skills – DynamoHome +# End-to-End Testing – DynamoHome -## Configuration +## Playwright configuration - **Config file**: `playwright.config.js` (`testDir: './tests/e2e'`) - **Browser**: Chromium only (Desktop Chrome profile) @@ -92,7 +92,6 @@ test('recent page shows graphs in grid view by default', async ({ page }) => { test('recent page switches to list view', async ({ page }) => { const recentPage = new RecentPage(page); await recentPage.switchToListView(); - // assert table is visible via page object method const isTableVisible = await recentPage.isTableVisible(); expect(isTableVisible).toBe(true); }); diff --git a/.claude/skills/feature-ui/SKILL.md b/.claude/skills/feature-ui/SKILL.md new file mode 100644 index 0000000..575d8b5 --- /dev/null +++ b/.claude/skills/feature-ui/SKILL.md @@ -0,0 +1,91 @@ +# UI Feature Workflow – DynamoHome + +Use this workflow when implementing a new UI feature or modifying existing UI. + +## Steps + +### 1. Identify scope + +Determine which module(s) are affected and read the existing components before making changes: + +| Module | Location | +|---|---| +| Recent files | `src/components/Recent/` | +| Sample graphs | `src/components/Samples/` | +| Learning resources | `src/components/Learning/` | +| Sidebar navigation | `src/components/Sidebar/` | +| Shared / reused | `src/components/Common/` | + +### 2. Add localized strings first + +Before writing any JSX with user-visible text: + +1. Add the key to `src/locales/en.json` +2. Add the same key (English value as placeholder) to all 13 other locale files +3. Key format: `module.element.descriptor` (e.g. `recent.filter.label.search`) + +See the `localization` skill for patterns and the full locale list. + +### 3. Implement the UI change + +- Functional component with explicit prop interface +- CSS Modules for styling (`styles['class-name']`) +- `` or `useIntl()` for all user-visible text +- No new libraries + +If adding a new component, create `ComponentName.tsx` + `ComponentName.module.css` together in the module folder. + +```tsx +interface MyFeatureProps { + value: string; + onChange: (v: string) => void; +} + +export const MyFeature = ({ value, onChange }: MyFeatureProps) => { + const intl = useIntl(); + return ( +
+ + onChange(e.target.value)} + placeholder={intl.formatMessage({ id: 'module.myfeature.placeholder' })} + /> +
+ ); +}; +``` + +### 4. Update SettingsContext if the feature needs persistence + +If the feature requires persisting a user preference (e.g. a new view mode or toggle): +- Add the new field to `SettingsContext.tsx` +- Update `saveHomePageSettings()` in `src/functions/utility.ts` +- Adding new fields to the settings JSON is safe; never rename or remove existing fields + +### 5. Write unit tests + +For every component created or modified, create/update `tests/unit/ComponentName.test.tsx`: +- Wrap with `IntlProvider` for components using `FormattedMessage` +- Test render output and user interactions +- Run: `npm run test:unit -- --coverage` +- Coverage must not decrease + +See the `unit-testing` skill for patterns. + +### 6. Verify build and lint + +```bash +npm run lint:check # Must pass with no errors +npm run build # Must produce a valid bundle +npm run test:unit # All tests must pass +``` + +### 7. Update E2E tests if the user flow changed + +If the feature adds a new page, navigation path, or interactive element: +- Add or update Page Object classes in `tests/e2e/pages/` or `tests/e2e/components/` +- Add test cases to `tests/e2e/e2e.test.ts` (orchestration only — no selectors in the test file) +- Run: `npm run test:e2e` (requires `npm run start` running in another terminal) + +See the `end-to-end-testing` skill for POM patterns. diff --git a/.claude/skills/localization.md b/.claude/skills/localization/SKILL.md similarity index 100% rename from .claude/skills/localization.md rename to .claude/skills/localization/SKILL.md diff --git a/.claude/skills/react.md b/.claude/skills/react/SKILL.md similarity index 100% rename from .claude/skills/react.md rename to .claude/skills/react/SKILL.md diff --git a/.claude/skills/refactor/SKILL.md b/.claude/skills/refactor/SKILL.md new file mode 100644 index 0000000..ce77ecf --- /dev/null +++ b/.claude/skills/refactor/SKILL.md @@ -0,0 +1,62 @@ +# Refactor Workflow – DynamoHome + +Use this workflow when restructuring code without changing its observable behavior. + +## Steps + +### 1. Define scope and goal + +Before touching code, identify: +- What is being refactored and why (readability, duplication, type safety, etc.) +- What the observable behavior is, so you can verify it is preserved afterward +- Which files are in scope — do not touch files outside the stated scope + +### 2. Establish a baseline + +Run the full test suite to confirm the starting state is green: + +```bash +npm run lint:check +npm run test:unit +npm run build +``` + +If any of these fail before you start, stop and fix them first (or confirm with the user). + +### 3. Refactor incrementally + +- Make one logical change at a time (extract a component, rename a type, consolidate duplicate logic) +- Run `npm run test:unit` after each meaningful change to confirm nothing broke +- Note each stable checkpoint — makes it easy to revert one step without losing all work + +### 4. DynamoHome-specific risks + +| Refactor | Risk to check | +|---|---| +| Renaming a prop or interface | Any callers using the old name? Grep `src/` for it | +| Moving a component to `Common/` | Update all import paths in consumers | +| Extracting a custom hook | Verify `useSettings()` context is still available if the hook needs it | +| Consolidating or renaming locale keys | Check all 14 locale files; grep `src/` for old key usages | +| Changing `utility.ts` function signatures | Verify `tests/__mocks__/chromeMock.ts` covers the new signature | +| Changing `SettingsContext` shape | Grep for `useSettings` and verify every callsite still works | +| Renaming a global window callback | **Never do this** — Dynamo calls them by name from .NET | + +### 5. Final verification + +```bash +npm run lint:check # No new lint errors +npm run test:unit # All tests pass +npm run build # Dev bundle builds +npm run bundle # Production bundle builds (if webpack config was touched) +``` + +## What NOT to do during a refactor + +- Do not add features or fix unrelated bugs — keep the diff focused +- Do not introduce new npm dependencies +- Do not change observable behavior (what the user sees or what Dynamo receives) +- Do not rename or remove global `window.*` callbacks +- Do not change the settings JSON schema field names +- Do not change the output bundle path + +If you discover a bug while refactoring, note it separately — do not fix it inline. It muddies the diff and makes review harder. diff --git a/.claude/skills/unit-testing.md b/.claude/skills/unit-testing/SKILL.md similarity index 100% rename from .claude/skills/unit-testing.md rename to .claude/skills/unit-testing/SKILL.md diff --git a/.claude/workflows/bugfix.md b/.claude/workflows/bugfix.md deleted file mode 100644 index 4816587..0000000 --- a/.claude/workflows/bugfix.md +++ /dev/null @@ -1,68 +0,0 @@ -# Bug Fix Workflow - -Use this workflow when diagnosing and fixing a bug in DynamoHome. - -## Steps - -### 1. Reproduce the issue - -Before touching any code: -- Identify the affected module: Recent, Samples, Learning, Sidebar, or layout -- Check if the bug occurs in dev mode (`npm run start`) or only inside Dynamo (WebView context) -- For dev-mode reproduction: start the server and observe the behavior with mock data -- For WebView-only bugs: the root cause is likely in `src/functions/utility.ts` or the `window.chrome?.webview` guard logic - -### 2. Locate the root cause - -```bash -# Find components related to the bug -grep -r "keyword" src/components/ - -# Check how data flows to the affected component -# Trace from window globals → component setState → render -``` - -Common bug locations: -- **Data not displaying**: check `receiveXxxDataFromDotNet` callback and JSON parsing -- **Localization broken**: check locale key exists in all 14 locale files -- **Settings not persisting**: check `saveHomePageSettings()` in `utility.ts` and SettingsContext -- **Crash on load**: check `window.chrome?.webview` optional chaining guard -- **Wrong view mode**: check `SettingsContext` and the `recentPageViewMode`/`samplesViewMode` values - -### 3. Apply the fix - -- Make the **smallest change** that fixes the issue — do not refactor adjacent code -- Do not change unrelated files -- If the fix touches `window` globals or the settings JSON schema, check Dynamo compatibility: - - Global callback names must not change - - Settings field names must not change (adding new fields is safe) - -### 4. Add a regression test - -For every bug fixed, add or update a test that would have caught it: - -```tsx -// tests/unit/ComponentName.test.tsx -it('does not crash when graphData is empty', () => { - render(); - expect(screen.getByText(/no recent files/i)).toBeInTheDocument(); -}); -``` - -Run: `npm run test:unit` - -### 5. Verify everything passes - -```bash -npm run lint:check # No new lint errors -npm run test:unit # All unit tests pass (including new regression test) -npm run build # Dev bundle still builds -``` - -For bugs involving the full user flow, also run: `npm run test:e2e` - -## Dynamo-specific considerations - -- If the bug only occurs inside Dynamo (not in dev mode), read `src/functions/utility.ts` carefully — all WebView calls go through there -- The `hostObjects.scriptObject` methods are async — missing `await` is a common bug source -- JSON parsing errors from `GetHomePageSettings()` are caught with try-catch in `LayoutContainer.tsx` — check that logic if settings-related diff --git a/.claude/workflows/feature-ui.md b/.claude/workflows/feature-ui.md deleted file mode 100644 index 7b5fdc5..0000000 --- a/.claude/workflows/feature-ui.md +++ /dev/null @@ -1,90 +0,0 @@ -# UI Feature Development Workflow - -Use this workflow when implementing a new UI feature or modifying existing UI in DynamoHome. - -## Steps - -### 1. Identify scope - -Determine which module(s) are affected: -- **Recent** → `src/components/Recent/` -- **Samples** → `src/components/Samples/` -- **Learning** → `src/components/Learning/` -- **Sidebar** → `src/components/Sidebar/` -- **Shared** → `src/components/Common/` - -Read the existing components in that module before making changes. - -### 2. Add or update localized strings - -Before writing any JSX with user-visible text: - -a. Add the key to `src/locales/en.json` -b. Add the same key (English value as placeholder) to all 13 other locale files -c. Key format: `module.element.descriptor` (e.g., `recent.filter.label.search`) - -### 3. Implement the UI change - -Follow the React skills (`skills/react.md`): -- Functional component with explicit prop interface -- CSS Modules for styling -- `` or `useIntl()` for all text -- No new libraries - -If adding a new component: -- Place it in the correct module folder (or `Common/` if shared) -- Create `ComponentName.tsx` + `ComponentName.module.css` together - -### 4. Update SettingsContext if needed - -If the feature requires persisting user preferences (e.g., a new view mode): -- Add the new field to `SettingsContext.tsx` -- Update the settings JSON schema in `src/functions/utility.ts` -- Note: settings schema is shared with Dynamo — adding fields is safe, removing/renaming is a breaking change - -### 5. Write or update unit tests - -For every component created or modified, create/update `tests/unit/ComponentName.test.tsx`: -- Wrap with `IntlProvider` for components using `FormattedMessage` -- Test what renders, test user interactions -- Run: `npm run test:unit -- --coverage` -- Coverage must not decrease - -### 6. Verify build and lint - -```bash -npm run lint:check # Must pass with no errors -npm run build # Must produce a valid bundle -npm run test:unit # All tests must pass -``` - -### 7. Update E2E tests (if user flow changed) - -If the feature changes a user-visible flow (new page, new navigation, new interactive element): -- Add or update Page Object classes in `tests/e2e/pages/` or `tests/e2e/components/` -- Add test cases to `tests/e2e/e2e.test.ts` (orchestration only — no selectors in test file) -- Run: `npm run test:e2e` (requires `npm run start` running) - -## Quick reference — common patterns - -```tsx -// New component skeleton -interface MyFeatureProps { - value: string; - onChange: (v: string) => void; -} - -export const MyFeature = ({ value, onChange }: MyFeatureProps) => { - const intl = useIntl(); - return ( -
- - onChange(e.target.value)} - placeholder={intl.formatMessage({ id: 'module.myfeature.placeholder' })} - /> -
- ); -}; -``` diff --git a/.claude/workflows/pr-review.md b/.claude/workflows/pr-review.md deleted file mode 100644 index d8ae694..0000000 --- a/.claude/workflows/pr-review.md +++ /dev/null @@ -1,78 +0,0 @@ -# PR Review Workflow - -Use this workflow when reviewing a pull request against the DynamoHome repository. - -## Steps - -### 1. Understand the scope - -Read the PR description and list of changed files. Identify: -- Which module(s) are touched: Recent, Samples, Learning, Sidebar, Common, build, tests -- Is it a feature, bugfix, refactor, or dependency update? -- Are there Dynamo integration touchpoints (window globals, settings schema, output path)? - -### 2. Review code changes - -Work through the `code-review` skill checklist (`skills/code-review.md`): - -**TypeScript**: no new `any`, no `@ts-ignore` - -**React**: functional components, explicit prop types, CSS Modules, no new libraries - -**Localization**: -- Every new user-visible string must be in `en.json` AND all 13 other locale files -- Keys follow `module.element.descriptor` format -- No hardcoded text in JSX - -**Dynamo integration**: -- `window.chrome?.webview` always optional-chained -- No rename/removal of global callbacks (`receiveGraphDataFromDotNet`, `setLocale`, etc.) -- No change to settings JSON field names or output bundle path - -### 3. Verify test coverage - -**Unit tests**: -- New/modified components have test files in `tests/unit/` -- Tests validate behavior, not implementation details -- Chrome globals not re-mocked (already in `tests/__mocks__/chromeMock.ts`) - -**E2E tests**: -- Changed user flows have Playwright test coverage -- `tests/e2e/e2e.test.ts` contains no selectors or direct page actions -- All selectors live in `tests/e2e/pages/` or `tests/e2e/components/` - -### 4. Check build integrity - -Confirm (or ask the author to confirm): -```bash -npm run lint:check # Passes -npm run test:unit # Passes -npm run build # Dev bundle succeeds -npm run bundle # Production bundle succeeds (for PRs touching webpack or build scripts) -``` - -### 5. Write feedback - -Structure feedback clearly: - -- **Blocker**: something that will break functionality, cause a regression, or violates a hard rule - > File and line number, what the problem is, what the fix should be -- **Recommendation**: optional improvement, clearly labeled as non-blocking - > Keep it brief; one issue per comment - -Do NOT: -- Request refactors for code not touched by the PR -- Flag style differences that match the surrounding file -- Block for pre-existing issues (legacy `any` types, missing tests for old components) -- Ask for architectural changes outside the PR scope - -## Common blockers to watch for - -| Issue | Why it blocks | -|---|---| -| Missing locale key in any of the 14 locale files | App throws at runtime for users of that locale | -| `window.chrome.webview` accessed without optional chaining | Crashes in dev mode / outside Dynamo | -| Global callback renamed or removed | Dynamo .NET host calls it by name — breaking change | -| Selector in `e2e.test.ts` directly | Violates POM requirement; makes tests brittle | -| New npm dependency without justification | Increases bundle size, adds supply chain risk | -| Output path changed from `dist/build/` | Dynamo integration breaks at runtime | diff --git a/.claude/workflows/refactor.md b/.claude/workflows/refactor.md deleted file mode 100644 index dd9cc0a..0000000 --- a/.claude/workflows/refactor.md +++ /dev/null @@ -1,64 +0,0 @@ -# Refactor Workflow - -Use this workflow when restructuring code without changing its behavior in DynamoHome. - -## Steps - -### 1. Define scope and goal - -Before touching code, clearly identify: -- What is being refactored and why (readability, duplication, type safety, etc.) -- What the observable behavior is (so you can verify it's preserved) -- What files are in scope — do not touch files outside the stated scope - -### 2. Establish a baseline - -Run the full test suite to confirm starting state is green: - -```bash -npm run lint:check -npm run test:unit -npm run build -``` - -If any of these fail before you start, stop and fix them first or confirm with the user. - -### 3. Refactor incrementally - -- Make one logical change at a time (e.g., extract a component, rename a type, consolidate duplicate logic) -- After each meaningful change, run tests to confirm nothing broke: - ```bash - npm run test:unit - ``` -- Commit (or note) each stable checkpoint — makes it easy to revert one step without losing all work - -### 4. Common DynamoHome refactors — watch for these risks - -| Refactor | Risk to check | -|---|---| -| Renaming a prop or interface | Are there any callers that use the old name? Run `grep -r "oldName" src/` | -| Moving a component to `Common/` | Update all import paths in components that use it | -| Extracting a custom hook | Verify `useSettings()` context is still available if the hook needs it | -| Consolidating locale keys | Check all 14 locale files if you rename/remove a key; also `grep -r "old.key" src/` to find all usages | -| Changing utility.ts functions | These call `window.chrome.webview.hostObjects.scriptObject` — verify mocks in `tests/__mocks__/chromeMock.ts` cover the new signature | -| Changing SettingsContext shape | All consumers use `useSettings()` — grep for `useSettings` and verify each callsite | - -### 5. Final verification - -```bash -npm run lint:check # No new lint errors -npm run test:unit # All tests pass -npm run build # Dev bundle builds -npm run bundle # Production bundle builds (if webpack config was touched) -``` - -### 6. Do not do during a refactor - -- Do not add new features or fix unrelated bugs — keep the diff focused on the refactor -- Do not introduce new npm dependencies -- Do not change observable behavior (what the user sees or what Dynamo receives) -- Do not rename or remove global window callbacks (`receiveGraphDataFromDotNet`, `setLocale`, etc.) -- Do not change the settings JSON schema (field names, types) -- Do not change the output bundle path - -If you discover a bug while refactoring, note it separately — don't fix it inline (it muddies the refactor diff and makes review harder). diff --git a/.playwright-cli/page-2026-03-26T16-45-43-389Z.yml b/.playwright-cli/page-2026-03-26T16-45-43-389Z.yml new file mode 100644 index 0000000..e572a75 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-45-43-389Z.yml @@ -0,0 +1,69 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [disabled] [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [ref=e53] [cursor=pointer]: + - img [ref=e55] + - generic [ref=e58]: + - generic [ref=e60] [cursor=pointer]: + - img [ref=e62] + - generic [ref=e64]: + - paragraph [ref=e65]: Graph Name + - paragraph [ref=e66] + - generic [ref=e68] [cursor=pointer]: + - img [ref=e70] + - generic [ref=e72]: + - paragraph [ref=e73]: Graph Name + - paragraph [ref=e74] + - generic [ref=e76] [cursor=pointer]: + - img [ref=e78] + - generic [ref=e80]: + - paragraph [ref=e81]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - paragraph [ref=e82]: 1/9/2024 6:24:35 PM + - generic [ref=e84] [cursor=pointer]: + - img [ref=e86] + - generic [ref=e88]: + - paragraph [ref=e89]: Graph Name + - paragraph [ref=e90]: 1/9/2024 6:24:35 PM + - generic [ref=e92] [cursor=pointer]: + - img [ref=e94] + - generic [ref=e96]: + - paragraph [ref=e97]: Graph Name + - paragraph [ref=e98] + - generic [ref=e100] [cursor=pointer]: + - img [ref=e102] + - generic [ref=e104]: + - paragraph [ref=e105]: Graph Name + - paragraph [ref=e106]: 1/9/2024 6:24:35 PM + - generic [ref=e108] [cursor=pointer]: + - img [ref=e110] + - generic [ref=e112]: + - paragraph [ref=e113]: Graph Name + - paragraph [ref=e114] \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-45-54-366Z.yml b/.playwright-cli/page-2026-03-26T16-45-54-366Z.yml new file mode 100644 index 0000000..e572a75 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-45-54-366Z.yml @@ -0,0 +1,69 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [disabled] [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [ref=e53] [cursor=pointer]: + - img [ref=e55] + - generic [ref=e58]: + - generic [ref=e60] [cursor=pointer]: + - img [ref=e62] + - generic [ref=e64]: + - paragraph [ref=e65]: Graph Name + - paragraph [ref=e66] + - generic [ref=e68] [cursor=pointer]: + - img [ref=e70] + - generic [ref=e72]: + - paragraph [ref=e73]: Graph Name + - paragraph [ref=e74] + - generic [ref=e76] [cursor=pointer]: + - img [ref=e78] + - generic [ref=e80]: + - paragraph [ref=e81]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - paragraph [ref=e82]: 1/9/2024 6:24:35 PM + - generic [ref=e84] [cursor=pointer]: + - img [ref=e86] + - generic [ref=e88]: + - paragraph [ref=e89]: Graph Name + - paragraph [ref=e90]: 1/9/2024 6:24:35 PM + - generic [ref=e92] [cursor=pointer]: + - img [ref=e94] + - generic [ref=e96]: + - paragraph [ref=e97]: Graph Name + - paragraph [ref=e98] + - generic [ref=e100] [cursor=pointer]: + - img [ref=e102] + - generic [ref=e104]: + - paragraph [ref=e105]: Graph Name + - paragraph [ref=e106]: 1/9/2024 6:24:35 PM + - generic [ref=e108] [cursor=pointer]: + - img [ref=e110] + - generic [ref=e112]: + - paragraph [ref=e113]: Graph Name + - paragraph [ref=e114] \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-46-10-498Z.yml b/.playwright-cli/page-2026-03-26T16-46-10-498Z.yml new file mode 100644 index 0000000..a90800c --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-46-10-498Z.yml @@ -0,0 +1,415 @@ +- generic [active] [ref=e1]: + - generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e116]: + - paragraph [ref=e118]: Samples + - generic [ref=e119]: + - button [disabled] [ref=e120] [cursor=pointer]: + - img [ref=e122] + - button [ref=e124] [cursor=pointer]: + - img [ref=e126] + - generic [ref=e130] [cursor=pointer]: + - generic [ref=e131]: Open file location + - img [ref=e134] + - generic [ref=e138]: + - generic [ref=e142] [cursor=pointer]: + - img [ref=e144] + - generic [ref=e146]: + - paragraph [ref=e147]: loco.dyn + - paragraph [ref=e148]: 1/9/2024 6:24:35 PM + - generic [ref=e152] [cursor=pointer]: + - img [ref=e154] + - generic [ref=e155]: + - paragraph [ref=e156]: lose.dyn + - paragraph [ref=e157] + - generic [ref=e158]: + - paragraph [ref=e160]: Basics + - generic [ref=e161]: + - generic [ref=e163] [cursor=pointer]: + - img [ref=e165] + - generic [ref=e166]: + - paragraph [ref=e167]: Basics_Basic01.dyn + - paragraph [ref=e168] + - generic [ref=e170] [cursor=pointer]: + - img [ref=e172] + - generic [ref=e173]: + - paragraph [ref=e174]: Basics_Basic02.dyn + - paragraph [ref=e175] + - generic [ref=e177] [cursor=pointer]: + - img [ref=e179] + - generic [ref=e180]: + - paragraph [ref=e181]: Basics_Basic03.dyn + - paragraph [ref=e182] + - generic [ref=e183]: + - paragraph [ref=e185]: Core + - generic [ref=e186]: + - generic [ref=e188] [cursor=pointer]: + - img [ref=e190] + - generic [ref=e191]: + - paragraph [ref=e192]: Core_AttractorPoint.dyn + - paragraph [ref=e193] + - generic [ref=e195] [cursor=pointer]: + - img [ref=e197] + - generic [ref=e198]: + - paragraph [ref=e199]: Core_CodeBlocks.dyn + - paragraph [ref=e200] + - generic [ref=e202] [cursor=pointer]: + - img [ref=e204] + - generic [ref=e205]: + - paragraph [ref=e206]: Core_ListAtLevel.dyn + - paragraph [ref=e207] + - generic [ref=e209] [cursor=pointer]: + - img [ref=e211] + - generic [ref=e212]: + - paragraph [ref=e213]: Core_ListLacing.dyn + - paragraph [ref=e214] + - generic [ref=e216] [cursor=pointer]: + - img [ref=e218] + - generic [ref=e219]: + - paragraph [ref=e220]: Core_Math.dyn + - paragraph [ref=e221] + - generic [ref=e223] [cursor=pointer]: + - img [ref=e225] + - generic [ref=e226]: + - paragraph [ref=e227]: Core_PassingFunctions.dyn + - paragraph [ref=e228] + - generic [ref=e230] [cursor=pointer]: + - img [ref=e232] + - generic [ref=e233]: + - paragraph [ref=e234]: Core_Python.dyn + - paragraph [ref=e235] + - generic [ref=e237] [cursor=pointer]: + - img [ref=e239] + - generic [ref=e240]: + - paragraph [ref=e241]: Core_RangeSyntax.dyn + - paragraph [ref=e242] + - generic [ref=e244] [cursor=pointer]: + - img [ref=e246] + - generic [ref=e247]: + - paragraph [ref=e248]: Core_Strings.dyn + - paragraph [ref=e249] + - generic [ref=e250]: + - paragraph [ref=e252]: Geometry + - generic [ref=e253]: + - generic [ref=e255] [cursor=pointer]: + - img [ref=e257] + - generic [ref=e258]: + - paragraph [ref=e259]: Geometry_Curves.dyn + - paragraph [ref=e260] + - generic [ref=e262] [cursor=pointer]: + - img [ref=e264] + - generic [ref=e265]: + - paragraph [ref=e266]: Geometry_Points.dyn + - paragraph [ref=e267] + - generic [ref=e269] [cursor=pointer]: + - img [ref=e271] + - generic [ref=e272]: + - paragraph [ref=e273]: Geometry_Solids.dyn + - paragraph [ref=e274] + - generic [ref=e276] [cursor=pointer]: + - img [ref=e278] + - generic [ref=e279]: + - paragraph [ref=e280]: Geometry_Surfaces.dyn + - paragraph [ref=e281] + - generic [ref=e282]: + - paragraph [ref=e284]: ImportExport + - generic [ref=e285]: + - generic [ref=e287] [cursor=pointer]: + - img [ref=e289] + - generic [ref=e290]: + - paragraph [ref=e291]: ImportExport_CSV to Stuff.dyn + - paragraph [ref=e292] + - generic [ref=e294] [cursor=pointer]: + - img [ref=e296] + - generic [ref=e297]: + - paragraph [ref=e298]: ImportExport_Data To Excel.dyn + - paragraph [ref=e299] + - generic [ref=e301] [cursor=pointer]: + - img [ref=e303] + - generic [ref=e304]: + - paragraph [ref=e305]: ImportExport_Excel to Dynamo.dyn + - paragraph [ref=e306] + - generic [ref=e308] [cursor=pointer]: + - img [ref=e310] + - generic [ref=e311]: + - paragraph [ref=e312]: OpenXMLExport_Data To Excel.dyn + - paragraph [ref=e313] + - generic [ref=e315] [cursor=pointer]: + - img [ref=e317] + - generic [ref=e318]: + - paragraph [ref=e319]: OpenXMLImport_Data From Excel.dyn + - paragraph [ref=e320] + - generic [ref=e321]: + - paragraph [ref=e323]: Revit + - generic [ref=e324]: + - generic [ref=e326] [cursor=pointer]: + - img [ref=e328] + - generic [ref=e329]: + - paragraph [ref=e330]: DynamoPlayer-7 + - paragraph [ref=e331] + - generic [ref=e333] [cursor=pointer]: + - img [ref=e335] + - generic [ref=e336]: + - paragraph [ref=e337]: Revit_Adaptive Component Placement.dyn + - paragraph [ref=e338] + - generic [ref=e340] [cursor=pointer]: + - img [ref=e342] + - generic [ref=e343]: + - paragraph [ref=e344]: Revit_Color.dyn + - paragraph [ref=e345] + - generic [ref=e347] [cursor=pointer]: + - img [ref=e349] + - generic [ref=e350]: + - paragraph [ref=e351]: Revit_Floors and Framing.dyn + - paragraph [ref=e352] + - generic [ref=e354] [cursor=pointer]: + - img [ref=e356] + - generic [ref=e357]: + - paragraph [ref=e358]: Revit_GeometryCreation_Curves.dyn + - paragraph [ref=e359] + - generic [ref=e361] [cursor=pointer]: + - img [ref=e363] + - generic [ref=e364]: + - paragraph [ref=e365]: Revit_GeometryCreation_Points.dyn + - paragraph [ref=e366] + - generic [ref=e368] [cursor=pointer]: + - img [ref=e370] + - generic [ref=e371]: + - paragraph [ref=e372]: Revit_GeometryCreation_Solids.dyn + - paragraph [ref=e373] + - generic [ref=e375] [cursor=pointer]: + - img [ref=e377] + - generic [ref=e378]: + - paragraph [ref=e379]: Revit_GeometryCreation_Surfaces.dyn + - paragraph [ref=e380] + - generic [ref=e382] [cursor=pointer]: + - img [ref=e384] + - generic [ref=e385]: + - paragraph [ref=e386]: Revit_ImportSolid.dyn + - paragraph [ref=e387] + - generic [ref=e389] [cursor=pointer]: + - img [ref=e391] + - generic [ref=e392]: + - paragraph [ref=e393]: Revit_PlaceFamiliesByLevel_Set Parameters.dyn + - paragraph [ref=e394] + - generic [ref=e396] [cursor=pointer]: + - img [ref=e398] + - generic [ref=e399]: + - paragraph [ref=e400]: Revit_StructuralFraming.dyn + - paragraph [ref=e401] + - generic [ref=e402]: + - paragraph [ref=e404]: AnalyticalAutomation + - generic [ref=e405]: + - generic [ref=e407] [cursor=pointer]: + - img [ref=e409] + - generic [ref=e410]: + - paragraph [ref=e411]: Analytical to Physical for Buildings.dyn + - paragraph [ref=e412] + - generic [ref=e414] [cursor=pointer]: + - img [ref=e416] + - generic [ref=e417]: + - paragraph [ref=e418]: Physical to Analytical for Buildings.dyn + - paragraph [ref=e419] + - generic [ref=e420]: + - paragraph [ref=e422]: SteelConnections + - generic [ref=e423]: + - generic [ref=e425] [cursor=pointer]: + - img [ref=e427] + - generic [ref=e428]: + - paragraph [ref=e429]: Apex haunch by ranges.dyn + - paragraph [ref=e430] + - generic [ref=e432] [cursor=pointer]: + - img [ref=e434] + - generic [ref=e435]: + - paragraph [ref=e436]: Apex Haunch.dyn + - paragraph [ref=e437] + - generic [ref=e439] [cursor=pointer]: + - img [ref=e441] + - generic [ref=e442]: + - paragraph [ref=e443]: Base Plate By Member End Forces.dyn + - paragraph [ref=e444] + - generic [ref=e446] [cursor=pointer]: + - img [ref=e448] + - generic [ref=e449]: + - paragraph [ref=e450]: Base plate by ranges.dyn + - paragraph [ref=e451] + - generic [ref=e453] [cursor=pointer]: + - img [ref=e455] + - generic [ref=e456]: + - paragraph [ref=e457]: Base Plate using sample connection type - Imperial.dyn + - paragraph [ref=e458] + - generic [ref=e460] [cursor=pointer]: + - img [ref=e462] + - generic [ref=e463]: + - paragraph [ref=e464]: Base Plate using sample connection type - Metric.dyn + - paragraph [ref=e465] + - generic [ref=e467] [cursor=pointer]: + - img [ref=e469] + - generic [ref=e470]: + - paragraph [ref=e471]: Base Plate.dyn + - paragraph [ref=e472] + - generic [ref=e474] [cursor=pointer]: + - img [ref=e476] + - generic [ref=e477]: + - paragraph [ref=e478]: Beam end to end by ranges.dyn + - paragraph [ref=e479] + - generic [ref=e481] [cursor=pointer]: + - img [ref=e483] + - generic [ref=e484]: + - paragraph [ref=e485]: Beam to beam by ranges.dyn + - paragraph [ref=e486] + - generic [ref=e488] [cursor=pointer]: + - img [ref=e490] + - generic [ref=e491]: + - paragraph [ref=e492]: Beam to Column by ranges.dyn + - paragraph [ref=e493] + - generic [ref=e495] [cursor=pointer]: + - img [ref=e497] + - generic [ref=e498]: + - paragraph [ref=e499]: Beam to Column web or flange by ranges.dyn + - paragraph [ref=e500] + - generic [ref=e502] [cursor=pointer]: + - img [ref=e504] + - generic [ref=e505]: + - paragraph [ref=e506]: Bracing I splice - additional object.dyn + - paragraph [ref=e507] + - generic [ref=e509] [cursor=pointer]: + - img [ref=e511] + - generic [ref=e512]: + - paragraph [ref=e513]: Clip angle - beam to beam.dyn + - paragraph [ref=e514] + - generic [ref=e516] [cursor=pointer]: + - img [ref=e518] + - generic [ref=e519]: + - paragraph [ref=e520]: Clip angle - beam to column By Analysis Results.dyn + - paragraph [ref=e521] + - generic [ref=e523] [cursor=pointer]: + - img [ref=e525] + - generic [ref=e526]: + - paragraph [ref=e527]: Clip angle - beam to column.dyn + - paragraph [ref=e528] + - generic [ref=e530] [cursor=pointer]: + - img [ref=e532] + - generic [ref=e533]: + - paragraph [ref=e534]: Column end to end by ranges.dyn + - paragraph [ref=e535] + - generic [ref=e537] [cursor=pointer]: + - img [ref=e539] + - generic [ref=e540]: + - paragraph [ref=e541]: Double brace by ranges.dyn + - paragraph [ref=e542] + - generic [ref=e544] [cursor=pointer]: + - img [ref=e546] + - generic [ref=e547]: + - paragraph [ref=e548]: Double Side Clip Angle.dyn + - paragraph [ref=e549] + - generic [ref=e551] [cursor=pointer]: + - img [ref=e553] + - generic [ref=e554]: + - paragraph [ref=e555]: End plate with bolts for railing posts.dyn + - paragraph [ref=e556] + - generic [ref=e558] [cursor=pointer]: + - img [ref=e560] + - generic [ref=e561]: + - paragraph [ref=e562]: End Plate.dyn + - paragraph [ref=e563] + - generic [ref=e565] [cursor=pointer]: + - img [ref=e567] + - generic [ref=e568]: + - paragraph [ref=e569]: Flange haunch - beam to column.dyn + - paragraph [ref=e570] + - generic [ref=e572] [cursor=pointer]: + - img [ref=e574] + - generic [ref=e575]: + - paragraph [ref=e576]: Front plate splice.dyn + - paragraph [ref=e577] + - generic [ref=e579] [cursor=pointer]: + - img [ref=e581] + - generic [ref=e582]: + - paragraph [ref=e583]: Gable Wall End Plate.dyn + - paragraph [ref=e584] + - generic [ref=e586] [cursor=pointer]: + - img [ref=e588] + - generic [ref=e589]: + - paragraph [ref=e590]: Gusset Plate at 1 Diagonal.dyn + - paragraph [ref=e591] + - generic [ref=e593] [cursor=pointer]: + - img [ref=e595] + - generic [ref=e596]: + - paragraph [ref=e597]: Moment connection - beam to column.dyn + - paragraph [ref=e598] + - generic [ref=e600] [cursor=pointer]: + - img [ref=e602] + - generic [ref=e603]: + - paragraph [ref=e604]: Platform connection - beam to beam.dyn + - paragraph [ref=e605] + - generic [ref=e607] [cursor=pointer]: + - img [ref=e609] + - generic [ref=e610]: + - paragraph [ref=e611]: Single brace to beam by ranges.dyn + - paragraph [ref=e612] + - generic [ref=e614] [cursor=pointer]: + - img [ref=e616] + - generic [ref=e617]: + - paragraph [ref=e618]: Single brace to Column by ranges.dyn + - paragraph [ref=e619] + - generic [ref=e621] [cursor=pointer]: + - img [ref=e623] + - generic [ref=e624]: + - paragraph [ref=e625]: Single tube brace.dyn + - paragraph [ref=e626] + - generic [ref=e628] [cursor=pointer]: + - img [ref=e630] + - generic [ref=e631]: + - paragraph [ref=e632]: Splice Joint.dyn + - paragraph [ref=e633] + - generic [ref=e635] [cursor=pointer]: + - img [ref=e637] + - generic [ref=e638]: + - paragraph [ref=e639]: Triple brace by ranges.dyn + - paragraph [ref=e640] + - generic [ref=e642] [cursor=pointer]: + - img [ref=e644] + - generic [ref=e645]: + - paragraph [ref=e646]: Two beams to one beam by ranges.dyn + - paragraph [ref=e647] + - generic [ref=e649] [cursor=pointer]: + - img [ref=e651] + - generic [ref=e652]: + - paragraph [ref=e653]: Two beams to one column by ranges.dyn + - paragraph [ref=e654] + - generic [ref=e656] [cursor=pointer]: + - img [ref=e658] + - generic [ref=e659]: + - paragraph [ref=e660]: Two beams to one column web or flange by ranges.dyn + - paragraph [ref=e661] + - generic [ref=e663] [cursor=pointer]: + - img [ref=e665] + - generic [ref=e666]: + - paragraph [ref=e667]: Web haunch - beam to column.dyn + - paragraph [ref=e668] + - generic [ref=e671]: View sample graphs \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-46-26-627Z.yml b/.playwright-cli/page-2026-03-26T16-46-26-627Z.yml new file mode 100644 index 0000000..6d674bd --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-46-26-627Z.yml @@ -0,0 +1,281 @@ +- generic [active] [ref=e1]: + - generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e673]: + - paragraph [ref=e675]: Learning + - generic [ref=e676]: + - paragraph [ref=e678]: Interactive Guides + - generic [ref=e679]: + - generic [ref=e681] [cursor=pointer]: + - img [ref=e683] + - generic [ref=e685]: + - paragraph [ref=e686]: User Interactive Tour + - paragraph [ref=e687]: Description + - generic [ref=e689] [cursor=pointer]: + - img [ref=e691] + - generic [ref=e693]: + - paragraph [ref=e694]: Getting Started + - paragraph [ref=e695]: Description + - generic [ref=e697] [cursor=pointer]: + - img [ref=e699] + - generic [ref=e701]: + - paragraph [ref=e702]: Packages + - paragraph [ref=e703]: Description + - generic [ref=e704]: + - paragraph [ref=e706]: Video Tutorials + - generic [ref=e707]: + - button [ref=e708] [cursor=pointer]: + - img [ref=e709] + - generic [ref=e712]: + - generic [ref=e714]: + - iframe [ref=e717]: + - generic "YouTube Video Player" [ref=f1e3]: + - generic [ref=f1e5]: + - link "Photo image of DynamoBIM" [ref=f1e8] [cursor=pointer]: + - /url: https://www.youtube.com/channel/UCsTIj3LQmIeOaWl4SVCidZw?embeds_referring_euri=http%3A%2F%2Flocalhost%3A8080%2F + - link "Lesson 01 - Getting Situated With Dynamo" [ref=f1e11] [cursor=pointer]: + - /url: https://www.youtube.com/watch?v=V_8WsDzim44 + - button "Play" [ref=f1e14] [cursor=pointer]: + - img + - generic [ref=e718]: + - paragraph [ref=e719] [cursor=pointer]: GETTING SITUATED WITH DYNAMO + - paragraph [ref=e721] [cursor=pointer]: In this course, we will provide an in-depth overview of Dynamo, a visual programming plugin for designers. We'll look at the node system and interoperability workflows to integrate computational design capabilities into your design workflows + - generic [ref=e723]: + - iframe [ref=e726]: + - generic "YouTube Video Player" [ref=f2e3]: + - generic [ref=f2e5]: + - link "Photo image of DynamoBIM" [ref=f2e8] [cursor=pointer]: + - /url: https://www.youtube.com/channel/UCsTIj3LQmIeOaWl4SVCidZw?embeds_referring_euri=http%3A%2F%2Flocalhost%3A8080%2F + - link "Lesson 02 - The Anatomy of a Dynamo Graph" [ref=f2e11] [cursor=pointer]: + - /url: https://www.youtube.com/watch?v=-2Uau6NpmXI + - button "Play" [ref=f2e14] [cursor=pointer]: + - img + - generic [ref=e727]: + - paragraph [ref=e728] [cursor=pointer]: THE ANATOMY OF A DYNAMO GRAPH + - paragraph [ref=e730] [cursor=pointer]: In this lesson, we will take a closer look at the specific elements used to create a Dynamo graph. By the end of this lesson, you will have an understanding of the components that comprise a Dynamo graph, the basics of defining an algorithm, and what it means to map together nodes to update results. + - generic [ref=e732]: + - iframe [ref=e735]: + - generic [active] [ref=f3e1]: + - generic "YouTube Video Player" [ref=f3e3] + - generic [ref=f3e5]: + - generic: + - generic: + - button "Play video" [ref=f3e10] [cursor=pointer]: + - generic [ref=f3e13]: + - img + - button "Hide player controls" [ref=f3e14] [cursor=pointer] + - generic [ref=f3e16]: + - generic [ref=f3e21]: + - generic [ref=f3e22]: + - 'link "Lesson 03 - Annotating and Documenting Graphs: Groups and Notes" [ref=f3e23] [cursor=pointer]': + - /url: https://www.youtube.com/watch?v=V2ZuWTutXXc + - link "DynamoBIM" [ref=f3e24] [cursor=pointer]: + - /url: /channel/UCsTIj3LQmIeOaWl4SVCidZw + - generic [ref=f3e25]: DynamoBIM + - generic [ref=f3e26]: + - button "thumbnail-image" [ref=f3e27] [cursor=pointer]: + - img "thumbnail-image" [ref=f3e28] + - generic [ref=f3e30]: + - generic: DynamoBIM + - generic: 5.26K subscribers + - generic [ref=f3e31]: + - button "Share" [ref=f3e34] [cursor=pointer]: + - generic [ref=f3e38]: + - img + - link "Watch on YouTube" [ref=f3e45] [cursor=pointer]: + - /url: https://www.youtube.com/watch?v=V2ZuWTutXXc + - generic [ref=f3e48]: + - text: Watch on + - img [ref=f3e50]: + - generic [ref=f3e52]: + - img + - generic [ref=e736]: + - paragraph [ref=e737] [cursor=pointer]: "ANNOTATING AND DOCUMENTING GRAPHS: GROUPS AND NOTES" + - paragraph [ref=e739] [cursor=pointer]: In this lesson, we're going to cover annotation strategies for your Dynamo graph. Annotating and cleaning up Dynamo graphs is a very important part of your Dynamo workflow. Specifically, if you're looking to share your graphs with anyone else, you want to be very clear and concise as to what your graph is doing. + - generic [ref=e741]: + - iframe [ref=e744]: + - generic "YouTube Video Player" [ref=f4e3]: + - generic [ref=f4e5]: + - link "Photo image of DynamoBIM" [ref=f4e8] [cursor=pointer]: + - /url: https://www.youtube.com/channel/UCsTIj3LQmIeOaWl4SVCidZw?embeds_referring_euri=http%3A%2F%2Flocalhost%3A8080%2F + - 'link "Lesson 04 - Annotating and Documenting: Wire Interactions and Graph Properties" [ref=f4e11] [cursor=pointer]': + - /url: https://www.youtube.com/watch?v=zf-n2QL0-AU + - button "Play" [ref=f4e14] [cursor=pointer]: + - img + - generic [ref=e745]: + - paragraph [ref=e746] [cursor=pointer]: THE ANATOMY OF A DYNAMO GRAPH + - paragraph [ref=e748] [cursor=pointer]: In this lesson, we're going to take a look at additional ways of cleaning up and documenting our Dynamo graphs. By the end of this lesson, you're going to learn ways to interact with the wires of a Dynamo graph and take a look at further documentation strategies for serializing properties within our Dynamo graphs. + - generic [ref=e750]: + - iframe [ref=e753]: + - generic [active] [ref=f5e1]: + - generic "YouTube Video Player" [ref=f5e3] + - generic [ref=f5e5]: + - generic: + - generic: + - button "Play video" [ref=f5e10] [cursor=pointer] + - button "Hide player controls" [ref=f5e12] [cursor=pointer] + - generic [ref=f5e14]: + - generic [ref=f5e19]: + - generic [ref=f5e20]: + - link "Lesson 05 - Data Management" [ref=f5e21] [cursor=pointer]: + - /url: https://www.youtube.com/watch?v=0urQIHKjvZA + - link "DynamoBIM" [ref=f5e22] [cursor=pointer]: + - /url: /channel/UCsTIj3LQmIeOaWl4SVCidZw + - generic [ref=f5e23]: DynamoBIM + - generic [ref=f5e24]: + - button [ref=f5e25] [cursor=pointer] + - generic [ref=f5e27]: + - generic: DynamoBIM + - generic: 5.26K subscribers + - generic [ref=f5e28]: + - button "Share" [ref=f5e31] [cursor=pointer]: + - generic [ref=f5e35]: + - img + - link "Watch on YouTube" [ref=f5e42] [cursor=pointer]: + - /url: https://www.youtube.com/watch?v=0urQIHKjvZA + - generic [ref=f5e45]: + - text: Watch on + - img [ref=f5e47]: + - generic [ref=f5e49]: + - img + - generic [ref=e754]: + - paragraph [ref=e755] [cursor=pointer]: DATA MANAGEMENT + - paragraph [ref=e757] [cursor=pointer]: In this lesson, we will discuss the basic concepts of creating and working with lists in Dynamo. In Dynamo, we can have lists that contain data of varying lengths and depths. By the end of this lesson, you will have an understanding of list structure in Dynamo, you'll know how to create a list of points, and you'll have an understanding of what lacing is + - generic [ref=e759]: + - iframe [ref=e762]: + - generic [active] [ref=f6e1]: + - generic "YouTube Video Player" [ref=f6e3] + - generic [ref=f6e5]: + - generic: + - generic: + - button "Play video" [ref=f6e10] [cursor=pointer] + - button "Hide player controls" [ref=f6e12] [cursor=pointer] + - generic [ref=f6e14]: + - generic [ref=f6e19]: + - generic [ref=f6e20]: + - link "Lesson 06 - Nested List Management" [ref=f6e21] [cursor=pointer]: + - /url: https://www.youtube.com/watch?v=8VZ_--LaemE + - link "DynamoBIM" [ref=f6e22] [cursor=pointer]: + - /url: /channel/UCsTIj3LQmIeOaWl4SVCidZw + - generic [ref=f6e23]: DynamoBIM + - generic [ref=f6e24]: + - button [ref=f6e25] [cursor=pointer] + - generic [ref=f6e27]: + - generic: DynamoBIM + - generic: 5.26K subscribers + - generic [ref=f6e28]: + - button "Share" [ref=f6e31] [cursor=pointer]: + - generic [ref=f6e35]: + - img + - link "Watch on YouTube" [ref=f6e42] [cursor=pointer]: + - /url: https://www.youtube.com/watch?v=8VZ_--LaemE + - generic [ref=f6e45]: + - text: Watch on + - img [ref=f6e47]: + - generic [ref=f6e49]: + - img + - generic [ref=e763]: + - paragraph [ref=e764] [cursor=pointer]: NESTED LIST MANAGEMENT + - paragraph [ref=e766] [cursor=pointer]: In this lesson, we will explore concepts for how to manipulate lists and nested lists in Dynamo. By the end of this lesson, you'll have an understanding of what a nested list is and how to obtain items within a nested list. + - generic [ref=e768]: + - iframe [ref=e771]: + - generic "YouTube Video Player" [ref=f7e3]: + - generic [ref=f7e5]: + - link "Photo image of DynamoBIM" [ref=f7e8] [cursor=pointer]: + - /url: https://www.youtube.com/channel/UCsTIj3LQmIeOaWl4SVCidZw?embeds_referring_euri=http%3A%2F%2Flocalhost%3A8080%2F + - link "Lesson 07 - Computational Logic" [ref=f7e11] [cursor=pointer]: + - /url: https://www.youtube.com/watch?v=OQU2v-uxSR8 + - button "Play" [ref=f7e14] [cursor=pointer]: + - img + - generic [ref=e772]: + - paragraph [ref=e773] [cursor=pointer]: COMPUTATIONAL LOGIC + - paragraph [ref=e775] [cursor=pointer]: In this lesson, we're going to cover topics to create a graph with computational logic using an attractor algorithm. While an attractor algorithm sounds intimidating, it is simply a mathematical approach to driving geometry by proximity to any given point that is considered an attractor point. By the end of this lesson, you will reinforce your understanding of lacing, learn how to create an attractor algorithm, and learn new ways to interact with Dynamo geometry. + - generic [ref=e777]: + - iframe [ref=e780]: + - generic [active] [ref=f8e1]: + - generic "YouTube Video Player" [ref=f8e3] + - generic [ref=f8e5]: + - generic: + - generic: + - button "Play video" [ref=f8e10] [cursor=pointer] + - button "Hide player controls" [ref=f8e12] [cursor=pointer] + - generic [ref=f8e14]: + - generic [ref=f8e19]: + - generic [ref=f8e20]: + - 'link "Lesson 08 - Connecting Revit to Excel: Exporting Room Data" [ref=f8e21] [cursor=pointer]': + - /url: https://www.youtube.com/watch?v=QO3-j9nav2M + - link "DynamoBIM" [ref=f8e22] [cursor=pointer]: + - /url: /channel/UCsTIj3LQmIeOaWl4SVCidZw + - generic [ref=f8e23]: DynamoBIM + - generic [ref=f8e24]: + - button [ref=f8e25] [cursor=pointer] + - generic [ref=f8e27]: + - generic: DynamoBIM + - generic: 5.26K subscribers + - generic [ref=f8e28]: + - button "Share" [ref=f8e31] [cursor=pointer]: + - generic [ref=f8e35]: + - img + - link "Watch on YouTube" [ref=f8e42] [cursor=pointer]: + - /url: https://www.youtube.com/watch?v=QO3-j9nav2M + - generic [ref=f8e45]: + - text: Watch on + - img [ref=f8e47]: + - generic [ref=f8e49]: + - img + - generic [ref=e781]: + - paragraph [ref=e782] [cursor=pointer]: CONNECTING REVIT TO EXCEL - EXPORTING ROOM DATA + - paragraph [ref=e784] [cursor=pointer]: In this lesson, we will look at the power behind connecting Dynamo within Revit to an external program, such as Excel, to be able to extract and manipulate data from the Revit environment. By the end of this lesson, you will have an understanding of how to collect Revit room data, how to prepare the data for export, and strategies for ensuring data remains consistent for the import process at a later time. + - generic [ref=e786]: + - iframe [ref=e789]: + - generic "YouTube Video Player" [ref=f9e3]: + - generic [ref=f9e5]: + - link "Photo image of DynamoBIM" [ref=f9e8] [cursor=pointer]: + - /url: https://www.youtube.com/channel/UCsTIj3LQmIeOaWl4SVCidZw?embeds_referring_euri=http%3A%2F%2Flocalhost%3A8080%2F + - 'link "Lesson 09 - Connecting Revit to Excel: Importing Room Data" [ref=f9e11] [cursor=pointer]': + - /url: https://www.youtube.com/watch?v=OK5gO4mPmcI + - button "Play" [ref=f9e14] [cursor=pointer]: + - img + - generic [ref=e790]: + - paragraph [ref=e791] [cursor=pointer]: CONNECTING REVIT TO EXCEL - IMPORTING ROOM DATA + - paragraph [ref=e793] [cursor=pointer]: In this lesson, we will cover strategies for reading data from Excel and relating it to Revit elements. Specifically, we're going to read corrected room names and update the related rooms in Revit. By the end of this lesson, you will know how to read Excel files with Dynamo, select Revit elements by their element ID, and update Revit element parameter values. + - generic [ref=e795]: + - iframe [ref=e798]: + - generic "YouTube Video Player" [ref=f10e3]: + - generic [ref=f10e5]: + - link "Photo image of DynamoBIM" [ref=f10e8] [cursor=pointer]: + - /url: https://www.youtube.com/channel/UCsTIj3LQmIeOaWl4SVCidZw?embeds_referring_euri=http%3A%2F%2Flocalhost%3A8080%2F + - link "Lesson 10 - Code Blocks for Quick Data Entry and Object Creation" [ref=f10e11] [cursor=pointer]: + - /url: https://www.youtube.com/watch?v=zoE8N7zXKPI + - button "Play" [ref=f10e14] [cursor=pointer]: + - img + - generic [ref=e799]: + - paragraph [ref=e800] [cursor=pointer]: CODE BLOCKS FOR QUICK DATA ENTRY AND OBJECT CREATION + - paragraph [ref=e802] [cursor=pointer]: In this lesson, we will look at code blocks in Dynamo. Code blocks are a unique feature in Dynamo that brings together visual programming with DesignScript. DesignScript is a text-based language for computational design. By the end of this lesson, you will know how to use code blocks for numerical entry, string entry, and sequence and ranges. + - button [ref=e803] [cursor=pointer]: + - img [ref=e804] + - generic [ref=e808]: View learning content \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-46-42-925Z.yml b/.playwright-cli/page-2026-03-26T16-46-42-925Z.yml new file mode 100644 index 0000000..f0395a1 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-46-42-925Z.yml @@ -0,0 +1,71 @@ +- generic [active] [ref=e1]: + - generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [disabled] [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [ref=e53] [cursor=pointer]: + - img [ref=e55] + - generic [ref=e58]: + - generic [ref=e60] [cursor=pointer]: + - img [ref=e62] + - generic [ref=e64]: + - paragraph [ref=e65]: Graph Name + - paragraph [ref=e66] + - generic [ref=e68] [cursor=pointer]: + - img [ref=e70] + - generic [ref=e72]: + - paragraph [ref=e73]: Graph Name + - paragraph [ref=e74] + - generic [ref=e76] [cursor=pointer]: + - img [ref=e78] + - generic [ref=e80]: + - paragraph [ref=e81]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - paragraph [ref=e82]: 1/9/2024 6:24:35 PM + - generic [ref=e84] [cursor=pointer]: + - img [ref=e86] + - generic [ref=e88]: + - paragraph [ref=e89]: Graph Name + - paragraph [ref=e90]: 1/9/2024 6:24:35 PM + - generic [ref=e92] [cursor=pointer]: + - img [ref=e94] + - generic [ref=e96]: + - paragraph [ref=e97]: Graph Name + - paragraph [ref=e98] + - generic [ref=e100] [cursor=pointer]: + - img [ref=e102] + - generic [ref=e104]: + - paragraph [ref=e105]: Graph Name + - paragraph [ref=e106]: 1/9/2024 6:24:35 PM + - generic [ref=e108] [cursor=pointer]: + - img [ref=e110] + - generic [ref=e112]: + - paragraph [ref=e113]: Graph Name + - paragraph [ref=e114] + - generic [ref=e811]: View recent files \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-46-44-464Z.yml b/.playwright-cli/page-2026-03-26T16-46-44-464Z.yml new file mode 100644 index 0000000..f0395a1 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-46-44-464Z.yml @@ -0,0 +1,71 @@ +- generic [active] [ref=e1]: + - generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [disabled] [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [ref=e53] [cursor=pointer]: + - img [ref=e55] + - generic [ref=e58]: + - generic [ref=e60] [cursor=pointer]: + - img [ref=e62] + - generic [ref=e64]: + - paragraph [ref=e65]: Graph Name + - paragraph [ref=e66] + - generic [ref=e68] [cursor=pointer]: + - img [ref=e70] + - generic [ref=e72]: + - paragraph [ref=e73]: Graph Name + - paragraph [ref=e74] + - generic [ref=e76] [cursor=pointer]: + - img [ref=e78] + - generic [ref=e80]: + - paragraph [ref=e81]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - paragraph [ref=e82]: 1/9/2024 6:24:35 PM + - generic [ref=e84] [cursor=pointer]: + - img [ref=e86] + - generic [ref=e88]: + - paragraph [ref=e89]: Graph Name + - paragraph [ref=e90]: 1/9/2024 6:24:35 PM + - generic [ref=e92] [cursor=pointer]: + - img [ref=e94] + - generic [ref=e96]: + - paragraph [ref=e97]: Graph Name + - paragraph [ref=e98] + - generic [ref=e100] [cursor=pointer]: + - img [ref=e102] + - generic [ref=e104]: + - paragraph [ref=e105]: Graph Name + - paragraph [ref=e106]: 1/9/2024 6:24:35 PM + - generic [ref=e108] [cursor=pointer]: + - img [ref=e110] + - generic [ref=e112]: + - paragraph [ref=e113]: Graph Name + - paragraph [ref=e114] + - generic [ref=e811]: View recent files \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-46-53-977Z.yml b/.playwright-cli/page-2026-03-26T16-46-53-977Z.yml new file mode 100644 index 0000000..07c66d7 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-46-53-977Z.yml @@ -0,0 +1,126 @@ +- generic [active] [ref=e1]: + - generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - generic [ref=e919]: List view \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-46-55-714Z.yml b/.playwright-cli/page-2026-03-26T16-46-55-714Z.yml new file mode 100644 index 0000000..07c66d7 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-46-55-714Z.yml @@ -0,0 +1,126 @@ +- generic [active] [ref=e1]: + - generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - generic [ref=e919]: List view \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-47-08-146Z.yml b/.playwright-cli/page-2026-03-26T16-47-08-146Z.yml new file mode 100644 index 0000000..46d43d4 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-47-08-146Z.yml @@ -0,0 +1,124 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-47-09-786Z.yml b/.playwright-cli/page-2026-03-26T16-47-09-786Z.yml new file mode 100644 index 0000000..46d43d4 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-47-09-786Z.yml @@ -0,0 +1,124 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-47-23-427Z.yml b/.playwright-cli/page-2026-03-26T16-47-23-427Z.yml new file mode 100644 index 0000000..0d998fa --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-47-23-427Z.yml @@ -0,0 +1,129 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e11] [cursor=pointer]: + - generic [ref=e12]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e920]: + - generic [ref=e921]: Open File + - generic [ref=e922]: Open Template + - generic [ref=e923]: Backup Locations + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-47-25-533Z.yml b/.playwright-cli/page-2026-03-26T16-47-25-533Z.yml new file mode 100644 index 0000000..0d998fa --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-47-25-533Z.yml @@ -0,0 +1,129 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e11] [cursor=pointer]: + - generic [ref=e12]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e920]: + - generic [ref=e921]: Open File + - generic [ref=e922]: Open Template + - generic [ref=e923]: Backup Locations + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-47-43-906Z.yml b/.playwright-cli/page-2026-03-26T16-47-43-906Z.yml new file mode 100644 index 0000000..0d998fa --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-47-43-906Z.yml @@ -0,0 +1,129 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e11] [cursor=pointer]: + - generic [ref=e12]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e920]: + - generic [ref=e921]: Open File + - generic [ref=e922]: Open Template + - generic [ref=e923]: Backup Locations + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-48-00-025Z.yml b/.playwright-cli/page-2026-03-26T16-48-00-025Z.yml new file mode 100644 index 0000000..0d998fa --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-48-00-025Z.yml @@ -0,0 +1,129 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e11] [cursor=pointer]: + - generic [ref=e12]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e920]: + - generic [ref=e921]: Open File + - generic [ref=e922]: Open Template + - generic [ref=e923]: Backup Locations + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-48-09-485Z.yml b/.playwright-cli/page-2026-03-26T16-48-09-485Z.yml new file mode 100644 index 0000000..46d43d4 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-48-09-485Z.yml @@ -0,0 +1,124 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-48-11-041Z.yml b/.playwright-cli/page-2026-03-26T16-48-11-041Z.yml new file mode 100644 index 0000000..46d43d4 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-48-11-041Z.yml @@ -0,0 +1,124 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e20] [cursor=pointer]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-48-18-913Z.yml b/.playwright-cli/page-2026-03-26T16-48-18-913Z.yml new file mode 100644 index 0000000..13a0548 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-48-18-913Z.yml @@ -0,0 +1,128 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e19] [cursor=pointer]: + - generic [ref=e20]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e924]: + - generic [ref=e925]: Workspace + - generic [ref=e926]: Custom Node + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe \ No newline at end of file diff --git a/.playwright-cli/page-2026-03-26T16-48-20-477Z.yml b/.playwright-cli/page-2026-03-26T16-48-20-477Z.yml new file mode 100644 index 0000000..13a0548 --- /dev/null +++ b/.playwright-cli/page-2026-03-26T16-48-20-477Z.yml @@ -0,0 +1,128 @@ +- generic [ref=e5]: + - generic [ref=e8]: + - generic [ref=e9]: + - paragraph [ref=e10]: Dynamo + - generic [ref=e12] [cursor=pointer]: + - generic [ref=e13]: Open + - img [ref=e17] + - generic [ref=e19] [cursor=pointer]: + - generic [ref=e20]: + - generic [ref=e21]: New + - img [ref=e25] + - generic [ref=e924]: + - generic [ref=e925]: Workspace + - generic [ref=e926]: Custom Node + - generic [ref=e27]: + - generic [ref=e29] [cursor=pointer]: Recent + - generic [ref=e31] [cursor=pointer]: Samples + - generic [ref=e33] [cursor=pointer]: Learning + - separator [ref=e35] + - generic [ref=e36]: + - link "Discussion Forum" [ref=e37] [cursor=pointer]: + - /url: https://forum.dynamobim.com/ + - link "Dynamo Website" [ref=e38] [cursor=pointer]: + - /url: https://dynamobim.org/ + - link "Dynamo Primer" [ref=e39] [cursor=pointer]: + - /url: https://primer2.dynamobim.org/ + - link "Github Repository" [ref=e40] [cursor=pointer]: + - /url: https://github.com/dynamods + - link "Send Issues" [ref=e41] [cursor=pointer]: + - /url: https://github.com/DynamoDS/Dynamo/issues + - generic [ref=e45]: + - paragraph [ref=e47]: Recent + - generic [ref=e48]: + - button [ref=e49] [cursor=pointer]: + - img [ref=e51] + - button [disabled] [ref=e53] [cursor=pointer]: + - img [ref=e55] + - table [ref=e814]: + - rowgroup [ref=e815]: + - row "Title Author Date Modified Location" [ref=e816]: + - columnheader "Title" [ref=e817]: + - text: Title + - separator [ref=e818] + - columnheader "Author" [ref=e819]: + - text: Author + - separator [ref=e820] + - columnheader "Date Modified" [ref=e821]: + - text: Date Modified + - separator [ref=e822] + - columnheader "Location" [ref=e823] [cursor=pointer] + - rowgroup [ref=e824]: + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e825] [cursor=pointer]: + - cell "Graph Name" [ref=e826]: + - generic [ref=e827]: + - img [ref=e830] + - generic [ref=e831]: Graph Name + - cell [ref=e832]: + - generic: + - paragraph + - cell [ref=e833] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e834]: + - generic [ref=e836]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name Dynamo Team C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e837] [cursor=pointer]: + - cell "Graph Name" [ref=e838]: + - generic [ref=e839]: + - img [ref=e842] + - generic [ref=e843]: Graph Name + - cell "Dynamo Team" [ref=e844]: + - paragraph [ref=e846]: Dynamo Team + - cell [ref=e847] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e848]: + - generic [ref=e850]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all Dynamo 1.x file format 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e851] [cursor=pointer]: + - cell "Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all" [ref=e852]: + - generic [ref=e853]: + - img [ref=e856] + - generic [ref=e857]: Graph.with.very.long.name.that.doesn't.fit.the.screen.at.all + - cell "Dynamo 1.x file format" [ref=e858]: + - generic [ref=e859]: + - paragraph [ref=e860]: Dynamo 1.x file format + - img [ref=e863] + - cell "1/9/2024 6:24:35 PM" [ref=e865] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e866]: + - generic [ref=e868]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e869] [cursor=pointer]: + - cell "Graph Name" [ref=e870]: + - generic [ref=e871]: + - img [ref=e874] + - generic [ref=e875]: Graph Name + - cell [ref=e876]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e877] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e878]: + - generic [ref=e880]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e881] [cursor=pointer]: + - cell "Graph Name" [ref=e882]: + - generic [ref=e883]: + - img [ref=e886] + - generic [ref=e887]: Graph Name + - cell [ref=e888]: + - generic: + - paragraph + - cell [ref=e889] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e890]: + - generic [ref=e892]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name 1/9/2024 6:24:35 PM C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e893] [cursor=pointer]: + - cell "Graph Name" [ref=e894]: + - generic [ref=e895]: + - img [ref=e898] + - generic [ref=e899]: Graph Name + - cell [ref=e900]: + - generic: + - paragraph + - cell "1/9/2024 6:24:35 PM" [ref=e901] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e902]: + - generic [ref=e904]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe + - row "Graph Name C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e905] [cursor=pointer]: + - cell "Graph Name" [ref=e906]: + - generic [ref=e907]: + - img [ref=e910] + - generic [ref=e911]: Graph Name + - cell [ref=e912]: + - generic: + - paragraph + - cell [ref=e913] + - cell "C:\\Users\\DeyanNenov\\Documents\\GitHub\\Dynamo\\bin\\AnyCPU\\Debug\\DynamoSandbox.exe" [ref=e914]: + - generic [ref=e916]: C:\Users\DeyanNenov\Documents\GitHub\Dynamo\bin\AnyCPU\Debug\DynamoSandbox.exe \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b43411f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,151 @@ +# DynamoHome + +DynamoHome is the start page for **Dynamo**, an Autodesk visual programming tool. It is a React 18 SPA that runs inside a **Chrome WebView (Edge WebView2)** embedded in the Dynamo desktop application — not in a standalone browser. It is published to npm as `@dynamods/dynamo-home`. + +## Architecture + +``` +App.tsx # IntlProvider (localization) + SettingsProvider +└── LayoutContainer.tsx # SplitPane: resizable sidebar + main content + ├── Sidebar.tsx # Left nav: page switching, custom dropdowns + │ └── CustomDropDown.tsx + └── MainContent.tsx # Renders active page based on sidebar state + ├── PageRecent.tsx # Recent Dynamo files — grid or table view + │ ├── GraphGridItem.tsx + │ └── GraphTable.tsx # react-table with custom cell renderers + │ ├── CustomNameCellRenderer.tsx + │ ├── CustomLocationCellRenderer.tsx + │ └── CustomAuthorCellRenderer.tsx + ├── PageSamples.tsx # Sample graphs — grid or table view + │ ├── SamplesGrid.tsx → SamplesGridItem.tsx + │ └── SamplesTable.tsx → CustomSampleFirstCellRenderer.tsx + └── PageLearning.tsx # Learning resources — guides + video carousels + ├── Carousel.tsx + ├── GuideGridItem.tsx + ├── ModalItem.tsx + └── VideoCarouselItem.tsx + +src/components/Common/ # Shared across modules + CardItem.tsx Arrow.tsx Tooltip.tsx Portal.tsx CustomIcons.tsx +``` + +- No routing library — page switching is state in `MainContent.tsx` +- No Redux or external state — React Context (`SettingsContext.tsx`) + `useState` only +- `public/index.html` has two roots: `#root` (React) and `#modal-root` (Portal target) + +## Data flow + +``` +Dynamo (.NET host) + │ calls window globals on the app: + ├─ window.receiveGraphDataFromDotNet(json) → PageRecent state + ├─ window.receiveSamplesDataFromDotNet(json) → PageSamples state + ├─ window.receiveTrainingVideoDataFromDotNet(json) → PageLearning state + ├─ window.receiveInteractiveGuidesDataFromDotNet(json) → PageLearning state + ├─ window.setLocale(locale) → re-renders with new locale + ├─ window.setHomePageSettings(settingsJson) → SettingsContext hydration + └─ window.setShowStartPageChanged(show) → loading overlay + + │ app calls back into Dynamo via: + └─ window.chrome.webview.hostObjects.scriptObject.* → all calls go through src/functions/utility.ts +``` + +**Never rename or remove the window globals** — Dynamo calls them by name from .NET. Any signature change is a breaking change. + +## Dynamo host API (`src/functions/utility.ts`) + +All backend calls are routed through `utility.ts`. Never call `scriptObject` directly from components. + +```typescript +openFile(path: string) // scriptObject.OpenFile() +startGuidedTour(guidedTour: string) // scriptObject.StartGuidedTour() +sideBarCommand(value: SidebarCommand) // routes to OpenWorkspace / ShowTemplate / NewWorkspace / etc. +showSamplesCommand(value: ShowSamplesCommand) // routes to ShowSampleFilesInFolder / ShowSampleDatasetsInFolder +saveHomePageSettings(settings: any) // scriptObject.SaveHomePageSettings(JSON) +``` + +On mount, `App.tsx` calls `scriptObject.ApplicationLoaded()` to signal the host that the UI is ready. + +Always guard WebView access: +```tsx +if (window.chrome?.webview) { + await scriptObject.OpenFile(path); +} else { + console.log('[DEV] OpenFile:', path); +} +``` + +## Data shapes (from Dynamo) + +```typescript +// Recent file entry +{ Name: string; Path: string; Author: string; TimeStamp: string; IsPinned: boolean } + +// Sample graph entry +{ Name: string; Description: string; Path: string; ImagePath: string } + +// Settings (persisted to Dynamo — safe to add fields, never rename/remove) +{ recentPageViewMode: 'grid' | 'list'; samplesViewMode: 'grid' | 'list'; sideBarWidth: number } +``` + +## Settings persistence + +`SettingsContext.tsx` holds `recentPageViewMode`, `samplesViewMode`, `sideBarWidth`. +Loaded via `GetHomePageSettings()` on init, saved via `saveHomePageSettings()` on change. +Consumed everywhere via the `useSettings()` hook — do not call `useContext(SettingsContext)` directly. + +## Development mode + +When `window.chrome?.webview` is absent (running via `npm start` outside Dynamo), the app loads mock data from: +- `src/assets/home.ts` — recent files mock +- `src/assets/samples.ts` — samples mock +- `src/assets/learning.ts` — learning content mock + +Tests run against the dev server (`npm start`) which serves this mock data automatically. + +## Localization + +- 14 locale files in `src/locales/`: `en, cs, de, es, fr, it, ja, ko, pl, pt-BR, ru, zh-Hans, zh-Hant` + `en-GB` +- Locale is set at runtime by Dynamo calling `window.setLocale(locale)` +- Locale identifiers from Dynamo use country-code format (`es-ES`, `de-DE`) — `localization.ts` maps these +- `en.json` is the source of truth; all other locale files must have the same keys +- Key format: `module.element.descriptor` (e.g. `recent.table.header.name`) + +## Build output + +- Entry: `src/index.tsx` → Output: `dist/build/index.bundle.js` + `dist/build/index.html` +- **The path `dist/build/index.bundle.js` is hardcoded in Dynamo — do not change it** +- `npm run build` = dev bundle, `npm run bundle` = production (minified), `npm run production` = bundle + copy metadata + +## CI/CD + +- GitHub Actions (`build.yml`): runs on every PR and push to `master` — build → unit tests → Playwright e2e +- Releases: `npm-publish.yml` publishes `@dynamods/dynamo-home` to npm on release creation +- Localization: `trigger_l10n_jenkins.yml` triggers Autodesk's L10N Jenkins pipeline on master push + +## Key constraints — never do these + +- Do not change the output path `dist/build/` or rename `index.bundle.js` +- Do not rename or remove any `window.*` global callbacks +- Do not change the settings JSON shape (adding fields is safe; renaming/removing is a breaking change) +- Do not add routing libraries, state management libraries (Redux, Zustand, etc.), or UI component libraries +- Do not enable `strict: true` in `tsconfig.json` — it breaks existing code +- Do not rename existing npm scripts — CI calls them by name +- Do not rely on browser APIs not available in Chromium/Edge WebView2 + +## Naming and folder conventions + +- Components: PascalCase (`GraphGridItem.tsx`), co-located CSS Module (`GraphGridItem.module.css`) +- CSS classes accessed via bracket notation: `styles['class-name']` +- Shared components (2+ modules): `src/components/Common/` +- Unit tests: `tests/unit/ComponentName.test.tsx` +- Hooks: `use` prefix (`useSettings`) +- Types/interfaces: PascalCase, defined in `types/index.d.ts` for globals, inline interfaces for component props + +## Scope discipline + +- Make the smallest change that satisfies the requirement +- Do not refactor adjacent code while implementing a feature +- Do not add dependencies without explicit user approval +- Do not change build config, test config, or CI as a side effect of a feature +- If you spot a bug or improvement outside task scope, mention it — don't fix it unilaterally diff --git a/package.json b/package.json index 1a6f9bf..d9db12b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "lint:check": "eslint src/ tests/", "lint:fix": "eslint src/ tests/ --fix", "test:unit": "NODE_ENV=test & jest tests/unit", - "test:e2e": "playwright test tests/e2e/e2e.test.ts", + "test:e2e": "playwright test tests/e2e/", "test": "npm run test:unit && npm run test:e2e", "start": "webpack serve --config webpack.config.ts", "build": "webpack --config webpack.config.ts --mode=development", diff --git a/playwright.config.js b/playwright.config.js index 9487dbc..2735d59 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -61,6 +61,7 @@ const config = { webServer: { command: 'npm run start', port: 8080, + reuseExistingServer: !process.env.CI, }, }; diff --git a/src/components/CLAUDE.md b/src/components/CLAUDE.md new file mode 100644 index 0000000..a4e7e99 --- /dev/null +++ b/src/components/CLAUDE.md @@ -0,0 +1,9 @@ +When creating or moving a component, choose the folder by usage: + +| Used by | Folder | +|---|---| +| One module (Recent, Samples, Learning, Sidebar) | `[Module]/ComponentName.tsx` | +| Two or more modules | `Common/ComponentName.tsx` | +| App-level (layout, context) | `src/components/ComponentName.tsx` | + +Every component file must have a co-located CSS Module in the same folder: `ComponentName.module.css`. Create both files together — never a component without its stylesheet or vice versa. diff --git a/src/components/Common/CardItem.tsx b/src/components/Common/CardItem.tsx index cb7eb2d..21f7bce 100644 --- a/src/components/Common/CardItem.tsx +++ b/src/components/Common/CardItem.tsx @@ -3,7 +3,7 @@ import styles from './CardItems.module.css'; export const CardItem = ({ imageSrc, onClick, tooltipContent, titleText, subtitleText }: CardItem) => { return ( -
+
diff --git a/src/components/Learning/Carousel.tsx b/src/components/Learning/Carousel.tsx index 2b97335..981c7f0 100644 --- a/src/components/Learning/Carousel.tsx +++ b/src/components/Learning/Carousel.tsx @@ -18,7 +18,7 @@ export const Carousel = ({ children }: {children: ReactNode}) => { return (
-
@@ -26,7 +26,7 @@ export const Carousel = ({ children }: {children: ReactNode}) => { {children}
-
diff --git a/src/components/Learning/PageLearning.tsx b/src/components/Learning/PageLearning.tsx index 2f4fac8..12c8ecd 100644 --- a/src/components/Learning/PageLearning.tsx +++ b/src/components/Learning/PageLearning.tsx @@ -57,24 +57,24 @@ export function LearningPage(){ }, []); return( -
+
-

+

-

+

-
+
{guides.map(guide => ( ))}

-
+
-

+

{videos.map(video => ( diff --git a/src/components/Recent/PageRecent.tsx b/src/components/Recent/PageRecent.tsx index 6711191..7b141ea 100644 --- a/src/components/Recent/PageRecent.tsx +++ b/src/components/Recent/PageRecent.tsx @@ -104,23 +104,25 @@ export const RecentPage = ({ setIsDisabled, recentPageViewMode }: RecentPage) => }; return( -
+
-

+

- - -
@@ -26,9 +26,9 @@ export const Carousel = ({ children }: {children: ReactNode}) => { {children}
-
); -}; \ No newline at end of file +}; diff --git a/tests/e2e/components/Sidebar.ts b/tests/e2e/components/Sidebar.ts index 69a72f3..5ed02bc 100644 --- a/tests/e2e/components/Sidebar.ts +++ b/tests/e2e/components/Sidebar.ts @@ -1,29 +1,29 @@ import { Page, Locator } from '@playwright/test'; export class Sidebar { - private readonly navRecent: Locator; - private readonly navSamples: Locator; - private readonly navLearning: Locator; - private readonly openDropdownToggle: Locator; - private readonly newDropdownToggle: Locator; + private readonly navRecent: Locator; + private readonly navSamples: Locator; + private readonly navLearning: Locator; + private readonly openDropdownToggle: Locator; + private readonly newDropdownToggle: Locator; - constructor(private page: Page) { - this.navRecent = page.locator('[data-testid="nav-recent"]'); - this.navSamples = page.locator('[data-testid="nav-samples"]'); - this.navLearning = page.locator('[data-testid="nav-learning"]'); - this.openDropdownToggle = page.locator('[data-testid="openDropdown-toggle"]'); - this.newDropdownToggle = page.locator('[data-testid="newDropdown-toggle"]'); - } + constructor(private page: Page) { + this.navRecent = page.locator('[data-testid="nav-recent"]'); + this.navSamples = page.locator('[data-testid="nav-samples"]'); + this.navLearning = page.locator('[data-testid="nav-learning"]'); + this.openDropdownToggle = page.locator('[data-testid="openDropdown-toggle"]'); + this.newDropdownToggle = page.locator('[data-testid="newDropdown-toggle"]'); + } - async navigateToRecent() { await this.navRecent.click(); } - async navigateToSamples() { await this.navSamples.click(); } - async navigateToLearning() { await this.navLearning.click(); } - async openOpenDropdown() { await this.openDropdownToggle.click(); } - async openNewDropdown() { await this.newDropdownToggle.click(); } + async navigateToRecent() { await this.navRecent.click(); } + async navigateToSamples() { await this.navSamples.click(); } + async navigateToLearning() { await this.navLearning.click(); } + async openOpenDropdown() { await this.openDropdownToggle.click(); } + async openNewDropdown() { await this.newDropdownToggle.click(); } - getOpenOption(index: number) { return this.page.locator(`#openDropdown-${index}`); } - getNewOption(index: number) { return this.page.locator(`#newDropdown-${index}`); } + getOpenOption(index: number) { return this.page.locator(`#openDropdown-${index}`); } + getNewOption(index: number) { return this.page.locator(`#newDropdown-${index}`); } - getOpenDropdownOptions() { return this.page.locator('[data-testid="openDropdown-dropdown"] .dropdown-options'); } - getNewDropdownOptions() { return this.page.locator('[data-testid="newDropdown-dropdown"] .dropdown-options'); } + getOpenDropdownOptions() { return this.page.locator('[data-testid="openDropdown-dropdown"] .dropdown-options'); } + getNewDropdownOptions() { return this.page.locator('[data-testid="newDropdown-dropdown"] .dropdown-options'); } } diff --git a/tests/e2e/learning.spec.ts b/tests/e2e/learning.spec.ts index 095c67d..d5d290b 100644 --- a/tests/e2e/learning.spec.ts +++ b/tests/e2e/learning.spec.ts @@ -3,70 +3,70 @@ import { Sidebar } from './components/Sidebar'; import { LearningPage } from './pages/LearningPage'; test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:8080/'); + await page.goto('http://localhost:8080/'); }); test.describe('Learning page — content', () => { - test('L-01: Learning page shows Interactive Guides section', async ({ page }) => { - const sidebar = new Sidebar(page); - const learningPage = new LearningPage(page); + test('L-01: Learning page shows Interactive Guides section', async ({ page }) => { + const sidebar = new Sidebar(page); + const learningPage = new LearningPage(page); - await sidebar.navigateToLearning(); + await sidebar.navigateToLearning(); - expect(await learningPage.isGuidesVisible()).toBe(true); - }); + expect(await learningPage.isGuidesVisible()).toBe(true); + }); - test('L-02: Interactive Guides section shows 3 guide cards', async ({ page }) => { - const sidebar = new Sidebar(page); - const learningPage = new LearningPage(page); + test('L-02: Interactive Guides section shows 3 guide cards', async ({ page }) => { + const sidebar = new Sidebar(page); + const learningPage = new LearningPage(page); - await sidebar.navigateToLearning(); + await sidebar.navigateToLearning(); - const count = await learningPage.getGuideCount(); - expect(count).toBe(3); - }); + const count = await learningPage.getGuideCount(); + expect(count).toBe(3); + }); - test('L-03: guide card titles are correct', async ({ page }) => { - const sidebar = new Sidebar(page); - const learningPage = new LearningPage(page); + test('L-03: guide card titles are correct', async ({ page }) => { + const sidebar = new Sidebar(page); + const learningPage = new LearningPage(page); - await sidebar.navigateToLearning(); + await sidebar.navigateToLearning(); - expect(await learningPage.getGuideTitle(0)).toContain('User Interactive Tour'); - expect(await learningPage.getGuideTitle(1)).toContain('Getting Started'); - expect(await learningPage.getGuideTitle(2)).toContain('Packages'); - }); + expect(await learningPage.getGuideTitle(0)).toContain('User Interactive Tour'); + expect(await learningPage.getGuideTitle(1)).toContain('Getting Started'); + expect(await learningPage.getGuideTitle(2)).toContain('Packages'); + }); - test('L-04: Video Tutorials carousel is present with navigation buttons', async ({ page }) => { - const sidebar = new Sidebar(page); - const learningPage = new LearningPage(page); + test('L-04: Video Tutorials carousel is present with navigation buttons', async ({ page }) => { + const sidebar = new Sidebar(page); + const learningPage = new LearningPage(page); - await sidebar.navigateToLearning(); + await sidebar.navigateToLearning(); - expect(await learningPage.isCarouselVisible()).toBe(true); - await expect(learningPage.getCarouselPrev()).toBeVisible(); - await expect(learningPage.getCarouselNext()).toBeVisible(); - }); + expect(await learningPage.isCarouselVisible()).toBe(true); + await expect(learningPage.getCarouselPrev()).toBeVisible(); + await expect(learningPage.getCarouselNext()).toBeVisible(); + }); }); test.describe('Carousel navigation', () => { - test('C-01: carousel next button is clickable', async ({ page }) => { - const sidebar = new Sidebar(page); - const learningPage = new LearningPage(page); + test('C-01: carousel next button is clickable', async ({ page }) => { + const sidebar = new Sidebar(page); + const learningPage = new LearningPage(page); - await sidebar.navigateToLearning(); + await sidebar.navigateToLearning(); - await expect(learningPage.getCarouselNext()).toBeEnabled(); - await learningPage.clickNextVideo(); - }); + await expect(learningPage.getCarouselNext()).toBeEnabled(); + await learningPage.clickNextVideo(); + }); - test('C-02: carousel prev button is clickable', async ({ page }) => { - const sidebar = new Sidebar(page); - const learningPage = new LearningPage(page); + test('C-02: carousel prev button is clickable', async ({ page }) => { + const sidebar = new Sidebar(page); + const learningPage = new LearningPage(page); - await sidebar.navigateToLearning(); + await sidebar.navigateToLearning(); - await expect(learningPage.getCarouselPrev()).toBeEnabled(); - await learningPage.clickPrevVideo(); - }); + await expect(learningPage.getCarouselPrev()).toBeEnabled(); + await learningPage.clickPrevVideo(); + }); }); diff --git a/tests/e2e/navigation.spec.ts b/tests/e2e/navigation.spec.ts index da20770..5507d28 100644 --- a/tests/e2e/navigation.spec.ts +++ b/tests/e2e/navigation.spec.ts @@ -5,52 +5,52 @@ import { SamplesPage } from './pages/SamplesPage'; import { LearningPage } from './pages/LearningPage'; test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:8080/'); + await page.goto('http://localhost:8080/'); }); test.describe('Navigation', () => { - test('N-01: app loads with Recent page active by default', async ({ page }) => { - const recentPage = new RecentPage(page); - const samplesPage = new SamplesPage(page); - const learningPage = new LearningPage(page); - - expect(await recentPage.isVisible()).toBe(true); - expect(await samplesPage.isVisible()).toBe(false); - expect(await learningPage.isVisible()).toBe(false); - }); - - test('N-02: clicking Samples in sidebar shows Samples page', async ({ page }) => { - const sidebar = new Sidebar(page); - const recentPage = new RecentPage(page); - const samplesPage = new SamplesPage(page); - - await sidebar.navigateToSamples(); - - expect(await samplesPage.isVisible()).toBe(true); - expect(await recentPage.isVisible()).toBe(false); - }); - - test('N-03: clicking Learning in sidebar shows Learning page', async ({ page }) => { - const sidebar = new Sidebar(page); - const recentPage = new RecentPage(page); - const learningPage = new LearningPage(page); - - await sidebar.navigateToLearning(); - - expect(await learningPage.isVisible()).toBe(true); - expect(await recentPage.isVisible()).toBe(false); - }); - - test('N-04: clicking Recent from Learning returns to Recent page', async ({ page }) => { - const sidebar = new Sidebar(page); - const recentPage = new RecentPage(page); - const learningPage = new LearningPage(page); - - await sidebar.navigateToLearning(); - expect(await learningPage.isVisible()).toBe(true); - - await sidebar.navigateToRecent(); - expect(await recentPage.isVisible()).toBe(true); - expect(await learningPage.isVisible()).toBe(false); - }); + test('N-01: app loads with Recent page active by default', async ({ page }) => { + const recentPage = new RecentPage(page); + const samplesPage = new SamplesPage(page); + const learningPage = new LearningPage(page); + + expect(await recentPage.isVisible()).toBe(true); + expect(await samplesPage.isVisible()).toBe(false); + expect(await learningPage.isVisible()).toBe(false); + }); + + test('N-02: clicking Samples in sidebar shows Samples page', async ({ page }) => { + const sidebar = new Sidebar(page); + const recentPage = new RecentPage(page); + const samplesPage = new SamplesPage(page); + + await sidebar.navigateToSamples(); + + expect(await samplesPage.isVisible()).toBe(true); + expect(await recentPage.isVisible()).toBe(false); + }); + + test('N-03: clicking Learning in sidebar shows Learning page', async ({ page }) => { + const sidebar = new Sidebar(page); + const recentPage = new RecentPage(page); + const learningPage = new LearningPage(page); + + await sidebar.navigateToLearning(); + + expect(await learningPage.isVisible()).toBe(true); + expect(await recentPage.isVisible()).toBe(false); + }); + + test('N-04: clicking Recent from Learning returns to Recent page', async ({ page }) => { + const sidebar = new Sidebar(page); + const recentPage = new RecentPage(page); + const learningPage = new LearningPage(page); + + await sidebar.navigateToLearning(); + expect(await learningPage.isVisible()).toBe(true); + + await sidebar.navigateToRecent(); + expect(await recentPage.isVisible()).toBe(true); + expect(await learningPage.isVisible()).toBe(false); + }); }); diff --git a/tests/e2e/pages/LearningPage.ts b/tests/e2e/pages/LearningPage.ts index 321ce33..19c1550 100644 --- a/tests/e2e/pages/LearningPage.ts +++ b/tests/e2e/pages/LearningPage.ts @@ -1,32 +1,32 @@ import { Page, Locator } from '@playwright/test'; export class LearningPage { - private readonly root: Locator; - private readonly guidesSection: Locator; - private readonly videosSection: Locator; - private readonly carouselPrev: Locator; - private readonly carouselNext: Locator; + private readonly root: Locator; + private readonly guidesSection: Locator; + private readonly videosSection: Locator; + private readonly carouselPrev: Locator; + private readonly carouselNext: Locator; - constructor(private page: Page) { - this.root = page.locator('[data-testid="page-learning"]'); - this.guidesSection = page.locator('[data-testid="guides-section"]'); - this.videosSection = page.locator('[data-testid="videos-section"]'); - this.carouselPrev = page.locator('[data-testid="carousel-prev"]'); - this.carouselNext = page.locator('[data-testid="carousel-next"]'); - } + constructor(private page: Page) { + this.root = page.locator('[data-testid="page-learning"]'); + this.guidesSection = page.locator('[data-testid="guides-section"]'); + this.videosSection = page.locator('[data-testid="videos-section"]'); + this.carouselPrev = page.locator('[data-testid="carousel-prev"]'); + this.carouselNext = page.locator('[data-testid="carousel-next"]'); + } - async isVisible() { return this.root.isVisible(); } - async isGuidesVisible() { return this.guidesSection.isVisible(); } - async isCarouselVisible() { return this.videosSection.isVisible(); } - async getGuideCount() { return this.guidesSection.locator('[data-testid="card-item"]').count(); } - async getGuideTitle(i: number) { - return this.guidesSection.locator('[data-testid="card-item"]').nth(i) - .locator('p').first().textContent(); - } - async clickNextVideo() { await this.carouselNext.click(); } - async clickPrevVideo() { await this.carouselPrev.click(); } - isPrevButtonVisible() { return this.carouselPrev.isVisible(); } - isNextButtonVisible() { return this.carouselNext.isVisible(); } - getCarouselPrev() { return this.carouselPrev; } - getCarouselNext() { return this.carouselNext; } + async isVisible() { return this.root.isVisible(); } + async isGuidesVisible() { return this.guidesSection.isVisible(); } + async isCarouselVisible() { return this.videosSection.isVisible(); } + async getGuideCount() { return this.guidesSection.locator('[data-testid="card-item"]').count(); } + async getGuideTitle(i: number) { + return this.guidesSection.locator('[data-testid="card-item"]').nth(i) + .locator('p').first().textContent(); + } + async clickNextVideo() { await this.carouselNext.click(); } + async clickPrevVideo() { await this.carouselPrev.click(); } + isPrevButtonVisible() { return this.carouselPrev.isVisible(); } + isNextButtonVisible() { return this.carouselNext.isVisible(); } + getCarouselPrev() { return this.carouselPrev; } + getCarouselNext() { return this.carouselNext; } } diff --git a/tests/e2e/pages/RecentPage.ts b/tests/e2e/pages/RecentPage.ts index 7f56db3..63a59bf 100644 --- a/tests/e2e/pages/RecentPage.ts +++ b/tests/e2e/pages/RecentPage.ts @@ -1,28 +1,28 @@ import { Page, Locator } from '@playwright/test'; export class RecentPage { - private readonly root: Locator; - private readonly gridToggle: Locator; - private readonly listToggle: Locator; - private readonly graphGrid: Locator; - private readonly table: Locator; + private readonly root: Locator; + private readonly gridToggle: Locator; + private readonly listToggle: Locator; + private readonly graphGrid: Locator; + private readonly table: Locator; - constructor(private page: Page) { - this.root = page.locator('[data-testid="page-recent"]'); - this.gridToggle = page.locator('[data-testid="page-recent"] [data-testid="view-toggle-grid"]'); - this.listToggle = page.locator('[data-testid="page-recent"] [data-testid="view-toggle-list"]'); - this.graphGrid = page.locator('[data-testid="graph-grid"]'); - this.table = page.locator('[data-testid="page-recent"] table'); - } + constructor(private page: Page) { + this.root = page.locator('[data-testid="page-recent"]'); + this.gridToggle = page.locator('[data-testid="page-recent"] [data-testid="view-toggle-grid"]'); + this.listToggle = page.locator('[data-testid="page-recent"] [data-testid="view-toggle-list"]'); + this.graphGrid = page.locator('[data-testid="graph-grid"]'); + this.table = page.locator('[data-testid="page-recent"] table'); + } - async isVisible() { return this.root.isVisible(); } - async switchToGridView() { await this.gridToggle.click(); } - async switchToListView() { await this.listToggle.click(); } - async isGridActive() { return (await this.gridToggle.getAttribute('disabled')) !== null; } - async isListActive() { return (await this.listToggle.getAttribute('disabled')) !== null; } - async getCardCount() { return this.graphGrid.locator('[data-testid="card-item"]').count(); } - async isTableVisible() { return this.table.isVisible(); } - async isGridVisible() { return this.graphGrid.isVisible(); } - getTableHeaders() { return this.table.locator('th'); } - getFirstCardTitle() { return this.graphGrid.locator('[data-testid="card-item"]').first().locator('p').first(); } + async isVisible() { return this.root.isVisible(); } + async switchToGridView() { await this.gridToggle.click(); } + async switchToListView() { await this.listToggle.click(); } + async isGridActive() { return (await this.gridToggle.getAttribute('disabled')) !== null; } + async isListActive() { return (await this.listToggle.getAttribute('disabled')) !== null; } + async getCardCount() { return this.graphGrid.locator('[data-testid="card-item"]').count(); } + async isTableVisible() { return this.table.isVisible(); } + async isGridVisible() { return this.graphGrid.isVisible(); } + getTableHeaders() { return this.table.locator('th'); } + getFirstCardTitle() { return this.graphGrid.locator('[data-testid="card-item"]').first().locator('p').first(); } } diff --git a/tests/e2e/pages/SamplesPage.ts b/tests/e2e/pages/SamplesPage.ts index dae7c4b..787e60f 100644 --- a/tests/e2e/pages/SamplesPage.ts +++ b/tests/e2e/pages/SamplesPage.ts @@ -1,26 +1,26 @@ import { Page, Locator } from '@playwright/test'; export class SamplesPage { - private readonly root: Locator; - private readonly gridToggle: Locator; - private readonly listToggle: Locator; - private readonly samplesGrid: Locator; - private readonly table: Locator; + private readonly root: Locator; + private readonly gridToggle: Locator; + private readonly listToggle: Locator; + private readonly samplesGrid: Locator; + private readonly table: Locator; - constructor(private page: Page) { - this.root = page.locator('[data-testid="page-samples"]'); - this.gridToggle = page.locator('[data-testid="page-samples"] [data-testid="view-toggle-grid"]'); - this.listToggle = page.locator('[data-testid="page-samples"] [data-testid="view-toggle-list"]'); - this.samplesGrid = page.locator('[data-testid="samples-grid"]'); - this.table = page.locator('[data-testid="page-samples"] table'); - } + constructor(private page: Page) { + this.root = page.locator('[data-testid="page-samples"]'); + this.gridToggle = page.locator('[data-testid="page-samples"] [data-testid="view-toggle-grid"]'); + this.listToggle = page.locator('[data-testid="page-samples"] [data-testid="view-toggle-list"]'); + this.samplesGrid = page.locator('[data-testid="samples-grid"]'); + this.table = page.locator('[data-testid="page-samples"] table'); + } - async isVisible() { return this.root.isVisible(); } - async switchToGridView() { await this.gridToggle.click(); } - async switchToListView() { await this.listToggle.click(); } - async isGridActive() { return (await this.gridToggle.getAttribute('disabled')) !== null; } - async isListActive() { return (await this.listToggle.getAttribute('disabled')) !== null; } - async getCardCount() { return this.samplesGrid.locator('[data-testid="card-item"]').count(); } - async isTableVisible() { return this.table.isVisible(); } - async isGridVisible() { return this.samplesGrid.isVisible(); } + async isVisible() { return this.root.isVisible(); } + async switchToGridView() { await this.gridToggle.click(); } + async switchToListView() { await this.listToggle.click(); } + async isGridActive() { return (await this.gridToggle.getAttribute('disabled')) !== null; } + async isListActive() { return (await this.listToggle.getAttribute('disabled')) !== null; } + async getCardCount() { return this.samplesGrid.locator('[data-testid="card-item"]').count(); } + async isTableVisible() { return this.table.isVisible(); } + async isGridVisible() { return this.samplesGrid.isVisible(); } } diff --git a/tests/e2e/recent.spec.ts b/tests/e2e/recent.spec.ts index 8e48ae6..b6fbe12 100644 --- a/tests/e2e/recent.spec.ts +++ b/tests/e2e/recent.spec.ts @@ -2,64 +2,64 @@ import { test, expect } from '@playwright/test'; import { RecentPage } from './pages/RecentPage'; test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:8080/'); + await page.goto('http://localhost:8080/'); }); test.describe('Recent page — view modes', () => { - test('R-01: Recent loads in grid view by default', async ({ page }) => { - const recentPage = new RecentPage(page); + test('R-01: Recent loads in grid view by default', async ({ page }) => { + const recentPage = new RecentPage(page); - expect(await recentPage.isGridActive()).toBe(true); - expect(await recentPage.isListActive()).toBe(false); - expect(await recentPage.isGridVisible()).toBe(true); - expect(await recentPage.isTableVisible()).toBe(false); - }); + expect(await recentPage.isGridActive()).toBe(true); + expect(await recentPage.isListActive()).toBe(false); + expect(await recentPage.isGridVisible()).toBe(true); + expect(await recentPage.isTableVisible()).toBe(false); + }); - test('R-02: clicking list toggle switches to list view', async ({ page }) => { - const recentPage = new RecentPage(page); + test('R-02: clicking list toggle switches to list view', async ({ page }) => { + const recentPage = new RecentPage(page); - await recentPage.switchToListView(); + await recentPage.switchToListView(); - expect(await recentPage.isListActive()).toBe(true); - expect(await recentPage.isGridActive()).toBe(false); - expect(await recentPage.isTableVisible()).toBe(true); - }); + expect(await recentPage.isListActive()).toBe(true); + expect(await recentPage.isGridActive()).toBe(false); + expect(await recentPage.isTableVisible()).toBe(true); + }); - test('R-03: list view table has correct column headers', async ({ page }) => { - const recentPage = new RecentPage(page); + test('R-03: list view table has correct column headers', async ({ page }) => { + const recentPage = new RecentPage(page); - await recentPage.switchToListView(); + await recentPage.switchToListView(); - const headers = recentPage.getTableHeaders(); - await expect(headers.nth(0)).toContainText('Title'); - await expect(headers.nth(1)).toContainText('Author'); - await expect(headers.nth(2)).toContainText('Date Modified'); - await expect(headers.nth(3)).toContainText('Location'); - }); + const headers = recentPage.getTableHeaders(); + await expect(headers.nth(0)).toContainText('Title'); + await expect(headers.nth(1)).toContainText('Author'); + await expect(headers.nth(2)).toContainText('Date Modified'); + await expect(headers.nth(3)).toContainText('Location'); + }); - test('R-04: clicking grid toggle from list view returns to grid', async ({ page }) => { - const recentPage = new RecentPage(page); + test('R-04: clicking grid toggle from list view returns to grid', async ({ page }) => { + const recentPage = new RecentPage(page); - await recentPage.switchToListView(); - await recentPage.switchToGridView(); + await recentPage.switchToListView(); + await recentPage.switchToGridView(); - expect(await recentPage.isGridActive()).toBe(true); - expect(await recentPage.isGridVisible()).toBe(true); - expect(await recentPage.isTableVisible()).toBe(false); - }); + expect(await recentPage.isGridActive()).toBe(true); + expect(await recentPage.isGridVisible()).toBe(true); + expect(await recentPage.isTableVisible()).toBe(false); + }); }); test.describe('Recent page — content', () => { - test('R-05: grid view shows at least one graph card', async ({ page }) => { - const recentPage = new RecentPage(page); + test('R-05: grid view shows at least one graph card', async ({ page }) => { + const recentPage = new RecentPage(page); - const count = await recentPage.getCardCount(); - expect(count).toBeGreaterThan(0); - }); + const count = await recentPage.getCardCount(); + expect(count).toBeGreaterThan(0); + }); - test('R-06: graph cards render a non-empty title', async ({ page }) => { - const recentPage = new RecentPage(page); + test('R-06: graph cards render a non-empty title', async ({ page }) => { + const recentPage = new RecentPage(page); - await expect(recentPage.getFirstCardTitle()).not.toBeEmpty(); - }); + await expect(recentPage.getFirstCardTitle()).not.toBeEmpty(); + }); }); diff --git a/tests/e2e/samples.spec.ts b/tests/e2e/samples.spec.ts index 20670ea..29ce213 100644 --- a/tests/e2e/samples.spec.ts +++ b/tests/e2e/samples.spec.ts @@ -3,54 +3,54 @@ import { Sidebar } from './components/Sidebar'; import { SamplesPage } from './pages/SamplesPage'; test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:8080/'); + await page.goto('http://localhost:8080/'); }); test.describe('Samples page — view modes', () => { - test('S-01: Samples loads in grid view by default', async ({ page }) => { - const sidebar = new Sidebar(page); - const samplesPage = new SamplesPage(page); + test('S-01: Samples loads in grid view by default', async ({ page }) => { + const sidebar = new Sidebar(page); + const samplesPage = new SamplesPage(page); - await sidebar.navigateToSamples(); + await sidebar.navigateToSamples(); - expect(await samplesPage.isGridActive()).toBe(true); - expect(await samplesPage.isListActive()).toBe(false); - expect(await samplesPage.isGridVisible()).toBe(true); - }); + expect(await samplesPage.isGridActive()).toBe(true); + expect(await samplesPage.isListActive()).toBe(false); + expect(await samplesPage.isGridVisible()).toBe(true); + }); - test('S-02: clicking list toggle switches to list view', async ({ page }) => { - const sidebar = new Sidebar(page); - const samplesPage = new SamplesPage(page); + test('S-02: clicking list toggle switches to list view', async ({ page }) => { + const sidebar = new Sidebar(page); + const samplesPage = new SamplesPage(page); - await sidebar.navigateToSamples(); - await samplesPage.switchToListView(); + await sidebar.navigateToSamples(); + await samplesPage.switchToListView(); - expect(await samplesPage.isListActive()).toBe(true); - expect(await samplesPage.isTableVisible()).toBe(true); - }); + expect(await samplesPage.isListActive()).toBe(true); + expect(await samplesPage.isTableVisible()).toBe(true); + }); - test('S-03: clicking grid toggle from list view returns to grid', async ({ page }) => { - const sidebar = new Sidebar(page); - const samplesPage = new SamplesPage(page); + test('S-03: clicking grid toggle from list view returns to grid', async ({ page }) => { + const sidebar = new Sidebar(page); + const samplesPage = new SamplesPage(page); - await sidebar.navigateToSamples(); - await samplesPage.switchToListView(); - await samplesPage.switchToGridView(); + await sidebar.navigateToSamples(); + await samplesPage.switchToListView(); + await samplesPage.switchToGridView(); - expect(await samplesPage.isGridActive()).toBe(true); - expect(await samplesPage.isGridVisible()).toBe(true); - expect(await samplesPage.isTableVisible()).toBe(false); - }); + expect(await samplesPage.isGridActive()).toBe(true); + expect(await samplesPage.isGridVisible()).toBe(true); + expect(await samplesPage.isTableVisible()).toBe(false); + }); }); test.describe('Samples page — content', () => { - test('S-04: grid view shows at least one sample card', async ({ page }) => { - const sidebar = new Sidebar(page); - const samplesPage = new SamplesPage(page); + test('S-04: grid view shows at least one sample card', async ({ page }) => { + const sidebar = new Sidebar(page); + const samplesPage = new SamplesPage(page); - await sidebar.navigateToSamples(); + await sidebar.navigateToSamples(); - const count = await samplesPage.getCardCount(); - expect(count).toBeGreaterThan(0); - }); + const count = await samplesPage.getCardCount(); + expect(count).toBeGreaterThan(0); + }); }); diff --git a/tests/e2e/sidebar.spec.ts b/tests/e2e/sidebar.spec.ts index b7e62fa..5873e78 100644 --- a/tests/e2e/sidebar.spec.ts +++ b/tests/e2e/sidebar.spec.ts @@ -2,42 +2,42 @@ import { test, expect } from '@playwright/test'; import { Sidebar } from './components/Sidebar'; test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:8080/'); + await page.goto('http://localhost:8080/'); }); test.describe('Sidebar dropdowns', () => { - test('D-01: Open dropdown shows 3 options', async ({ page }) => { - const sidebar = new Sidebar(page); + test('D-01: Open dropdown shows 3 options', async ({ page }) => { + const sidebar = new Sidebar(page); - await sidebar.openOpenDropdown(); + await sidebar.openOpenDropdown(); - await expect(sidebar.getOpenOption(0)).toBeVisible(); - await expect(sidebar.getOpenOption(1)).toBeVisible(); - await expect(sidebar.getOpenOption(2)).toBeVisible(); - await expect(sidebar.getOpenOption(0)).toContainText('Open File'); - await expect(sidebar.getOpenOption(1)).toContainText('Open Template'); - await expect(sidebar.getOpenOption(2)).toContainText('Backup Locations'); - }); + await expect(sidebar.getOpenOption(0)).toBeVisible(); + await expect(sidebar.getOpenOption(1)).toBeVisible(); + await expect(sidebar.getOpenOption(2)).toBeVisible(); + await expect(sidebar.getOpenOption(0)).toContainText('Open File'); + await expect(sidebar.getOpenOption(1)).toContainText('Open Template'); + await expect(sidebar.getOpenOption(2)).toContainText('Backup Locations'); + }); - test('D-02: selecting an Open dropdown option closes the dropdown', async ({ page }) => { - const sidebar = new Sidebar(page); + test('D-02: selecting an Open dropdown option closes the dropdown', async ({ page }) => { + const sidebar = new Sidebar(page); - await sidebar.openOpenDropdown(); - await expect(sidebar.getOpenOption(0)).toBeVisible(); + await sidebar.openOpenDropdown(); + await expect(sidebar.getOpenOption(0)).toBeVisible(); - await sidebar.getOpenOption(0).click(); + await sidebar.getOpenOption(0).click(); - await expect(sidebar.getOpenOption(0)).not.toBeVisible(); - }); + await expect(sidebar.getOpenOption(0)).not.toBeVisible(); + }); - test('D-03: New dropdown shows 2 options', async ({ page }) => { - const sidebar = new Sidebar(page); + test('D-03: New dropdown shows 2 options', async ({ page }) => { + const sidebar = new Sidebar(page); - await sidebar.openNewDropdown(); + await sidebar.openNewDropdown(); - await expect(sidebar.getNewOption(0)).toBeVisible(); - await expect(sidebar.getNewOption(1)).toBeVisible(); - await expect(sidebar.getNewOption(0)).toContainText('Workspace'); - await expect(sidebar.getNewOption(1)).toContainText('Custom Node'); - }); + await expect(sidebar.getNewOption(0)).toBeVisible(); + await expect(sidebar.getNewOption(1)).toBeVisible(); + await expect(sidebar.getNewOption(0)).toContainText('Workspace'); + await expect(sidebar.getNewOption(1)).toContainText('Custom Node'); + }); }); diff --git a/tests/unit/App.test.tsx b/tests/unit/App.test.tsx index 1c096b5..3b31292 100644 --- a/tests/unit/App.test.tsx +++ b/tests/unit/App.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen, act } from '@testing-library/react'; import App from '../../src/App'; diff --git a/tests/unit/Common/Arrow.test.tsx b/tests/unit/Common/Arrow.test.tsx index d458c92..96f0b0d 100644 --- a/tests/unit/Common/Arrow.test.tsx +++ b/tests/unit/Common/Arrow.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render } from '@testing-library/react'; import { ClosedArrow, OpenArrow } from '../../../src/components/Common/Arrow'; diff --git a/tests/unit/Common/CardItem.test.tsx b/tests/unit/Common/CardItem.test.tsx index 1003819..f0f07ad 100644 --- a/tests/unit/Common/CardItem.test.tsx +++ b/tests/unit/Common/CardItem.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { CardItem } from '../../../src/components/Common/CardItem'; diff --git a/tests/unit/Common/CustomIcons.test.tsx b/tests/unit/Common/CustomIcons.test.tsx index 538d19c..78d5a93 100644 --- a/tests/unit/Common/CustomIcons.test.tsx +++ b/tests/unit/Common/CustomIcons.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render } from '@testing-library/react'; import { GridViewIcon, ListViewIcon, QuestionMarkIcon } from '../../../src/components/Common/CustomIcons'; diff --git a/tests/unit/Common/Portal.test.tsx b/tests/unit/Common/Portal.test.tsx index 2a6ca4e..67569d4 100644 --- a/tests/unit/Common/Portal.test.tsx +++ b/tests/unit/Common/Portal.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render } from '@testing-library/react'; import Portal from '../../../src/components/Common/Portal'; diff --git a/tests/unit/Common/Tooltip.test.tsx b/tests/unit/Common/Tooltip.test.tsx index a3b046c..039bfe9 100644 --- a/tests/unit/Common/Tooltip.test.tsx +++ b/tests/unit/Common/Tooltip.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import { Tooltip } from '../../../src/components/Common/Tooltip'; diff --git a/tests/unit/LayoutContainer.test.tsx b/tests/unit/LayoutContainer.test.tsx index 7c70f20..0746f39 100644 --- a/tests/unit/LayoutContainer.test.tsx +++ b/tests/unit/LayoutContainer.test.tsx @@ -1,9 +1,9 @@ -import React from 'react'; import { render, screen, fireEvent, act } from '@testing-library/react'; import { IntlProvider } from 'react-intl'; import { SettingsProvider } from '../../src/components/SettingsContext'; import { LayoutContainer } from '../../src/components/LayoutContainer'; import { getMessagesForLocale } from '../../src/localization/localization'; +import { saveHomePageSettings } from '../../src/functions/utility'; jest.mock('react-split-pane', () => { return function SplitPaneMock({ children, onDragFinished }: any) { @@ -130,7 +130,6 @@ describe('LayoutContainer', () => { }); it('triggering resize calls saveHomePageSettings', async () => { - const { saveHomePageSettings } = require('../../src/functions/utility'); renderLayout(); await act(async () => { fireEvent.click(screen.getByTestId('trigger-resize')); diff --git a/tests/unit/Learning/Carousel.test.tsx b/tests/unit/Learning/Carousel.test.tsx index c14f0cc..c03b580 100644 --- a/tests/unit/Learning/Carousel.test.tsx +++ b/tests/unit/Learning/Carousel.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { Carousel } from '../../../src/components/Learning/Carousel'; diff --git a/tests/unit/Learning/GuideGridItem.test.tsx b/tests/unit/Learning/GuideGridItem.test.tsx index a23a5b8..b9cfdf1 100644 --- a/tests/unit/Learning/GuideGridItem.test.tsx +++ b/tests/unit/Learning/GuideGridItem.test.tsx @@ -1,13 +1,11 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; +import { startGuidedTour } from '../../../src/functions/utility'; import { GuideGridItem } from '../../../src/components/Learning/GuideGridItem'; jest.mock('../../../src/functions/utility', () => ({ startGuidedTour: jest.fn(), })); -const { startGuidedTour } = require('../../../src/functions/utility'); - const defaultProps: Guide = { id: 'guide-1', Name: 'Geometry Guide', diff --git a/tests/unit/Learning/ModalItem.test.tsx b/tests/unit/Learning/ModalItem.test.tsx index 274b3d6..d8ea019 100644 --- a/tests/unit/Learning/ModalItem.test.tsx +++ b/tests/unit/Learning/ModalItem.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import ModalItem from '../../../src/components/Learning/ModalItem'; diff --git a/tests/unit/Learning/PageLearning.test.tsx b/tests/unit/Learning/PageLearning.test.tsx index c5b885f..10183dc 100644 --- a/tests/unit/Learning/PageLearning.test.tsx +++ b/tests/unit/Learning/PageLearning.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { screen, act } from '@testing-library/react'; import { renderWithIntl } from '../testUtils'; import { LearningPage } from '../../../src/components/Learning/PageLearning'; diff --git a/tests/unit/Learning/VideoCarouselItem.test.tsx b/tests/unit/Learning/VideoCarouselItem.test.tsx index 2ae793d..0f6b33b 100644 --- a/tests/unit/Learning/VideoCarouselItem.test.tsx +++ b/tests/unit/Learning/VideoCarouselItem.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { VideoCarouselItem } from '../../../src/components/Learning/VideoCarouselItem'; @@ -33,7 +32,7 @@ describe('VideoCarouselItem', () => { it('renders an iframe with the correct YouTube embed URL', () => { const { container } = render(); const iframes = container.querySelectorAll('iframe'); - const youtubeUrl = `https://www.youtube.com/embed/abc123xyz?autoplay=1`; + const youtubeUrl = 'https://www.youtube.com/embed/abc123xyz?autoplay=1'; const hasYoutubeIframe = Array.from(iframes).some( (iframe) => iframe.getAttribute('src') === youtubeUrl ); diff --git a/tests/unit/MainContent.test.tsx b/tests/unit/MainContent.test.tsx index fec886e..5e4a02e 100644 --- a/tests/unit/MainContent.test.tsx +++ b/tests/unit/MainContent.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen } from '@testing-library/react'; import { IntlProvider } from 'react-intl'; import { getMessagesForLocale } from '../../src/localization/localization'; diff --git a/tests/unit/Recent/CustomAuthorCellRenderer.test.tsx b/tests/unit/Recent/CustomAuthorCellRenderer.test.tsx index 8439622..65701df 100644 --- a/tests/unit/Recent/CustomAuthorCellRenderer.test.tsx +++ b/tests/unit/Recent/CustomAuthorCellRenderer.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { screen } from '@testing-library/react'; import { renderWithIntl } from '../testUtils'; import { CustomAuthorCellRenderer } from '../../../src/components/Recent/CustomAuthorCellRenderer'; diff --git a/tests/unit/Recent/CustomLocationCellRenderer.test.tsx b/tests/unit/Recent/CustomLocationCellRenderer.test.tsx index bae151b..c926621 100644 --- a/tests/unit/Recent/CustomLocationCellRenderer.test.tsx +++ b/tests/unit/Recent/CustomLocationCellRenderer.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { CustomLocationCellRenderer } from '../../../src/components/Recent/CustomLocationCellRenderer'; diff --git a/tests/unit/Recent/CustomNameCellRenderer.test.tsx b/tests/unit/Recent/CustomNameCellRenderer.test.tsx index 6cd9545..806a11a 100644 --- a/tests/unit/Recent/CustomNameCellRenderer.test.tsx +++ b/tests/unit/Recent/CustomNameCellRenderer.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { CustomNameCellRenderer } from '../../../src/components/Recent/CustomNameCellRenderer'; diff --git a/tests/unit/Recent/GraphGridItem.test.tsx b/tests/unit/Recent/GraphGridItem.test.tsx index ecd5200..b9324a9 100644 --- a/tests/unit/Recent/GraphGridItem.test.tsx +++ b/tests/unit/Recent/GraphGridItem.test.tsx @@ -1,5 +1,5 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; +import { openFile } from '../../../src/functions/utility'; import { GraphGridItem } from '../../../src/components/Recent/GraphGridItem'; jest.mock('../../../src/functions/utility', () => ({ @@ -7,8 +7,6 @@ jest.mock('../../../src/functions/utility', () => ({ saveHomePageSettings: jest.fn(), })); -const { openFile } = require('../../../src/functions/utility'); - const defaultProps = { id: 'graph-1', Caption: 'My Graph', diff --git a/tests/unit/Recent/GraphTable.test.tsx b/tests/unit/Recent/GraphTable.test.tsx index db77160..c40dfc8 100644 --- a/tests/unit/Recent/GraphTable.test.tsx +++ b/tests/unit/Recent/GraphTable.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { GraphTable } from '../../../src/components/Recent/GraphTable'; diff --git a/tests/unit/Recent/PageRecent.test.tsx b/tests/unit/Recent/PageRecent.test.tsx index 8c46dbe..3fc01f4 100644 --- a/tests/unit/Recent/PageRecent.test.tsx +++ b/tests/unit/Recent/PageRecent.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { screen, fireEvent, act } from '@testing-library/react'; import { renderWithProviders } from '../testUtils'; import { RecentPage } from '../../../src/components/Recent/PageRecent'; diff --git a/tests/unit/Samples/CustomSampleFirstCellRenderer.test.tsx b/tests/unit/Samples/CustomSampleFirstCellRenderer.test.tsx index ab931f8..fce5df3 100644 --- a/tests/unit/Samples/CustomSampleFirstCellRenderer.test.tsx +++ b/tests/unit/Samples/CustomSampleFirstCellRenderer.test.tsx @@ -1,9 +1,9 @@ -import React from 'react'; +import type { FC } from 'react'; import { render, screen } from '@testing-library/react'; import { CustomSampleFirstCellRenderer } from '../../../src/components/Samples/CustomSampleFirstCellRenderer'; // Cast so TypeScript doesn't complain about the string return case -const Renderer = CustomSampleFirstCellRenderer as React.FC; +const Renderer = CustomSampleFirstCellRenderer as FC; const makeRow = (overrides: Partial = {}): Row => ({ original: { diff --git a/tests/unit/Samples/PageSamples.test.tsx b/tests/unit/Samples/PageSamples.test.tsx index 25219c4..a641991 100644 --- a/tests/unit/Samples/PageSamples.test.tsx +++ b/tests/unit/Samples/PageSamples.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { screen, fireEvent, act } from '@testing-library/react'; import { renderWithProviders } from '../testUtils'; import { SamplesPage } from '../../../src/components/Samples/PageSamples'; @@ -16,7 +15,7 @@ jest.mock('../../../src/components/Samples/SamplesTable', () => ({ })); jest.mock('../../../src/components/Samples/SamplesGrid', () => ({ - SamplesGrid: ({ data }: any) =>
SamplesGrid
, + SamplesGrid: () =>
SamplesGrid
, })); jest.mock('../../../src/functions/utility', () => ({ @@ -25,12 +24,6 @@ jest.mock('../../../src/functions/utility', () => ({ saveHomePageSettings: jest.fn(), })); -const { openFile, showSamplesCommand } = require('../../../src/functions/utility'); - -const mockSamples = [ - { FileName: 'Sample One', FilePath: '/path/one.dyn', Description: '', DateModified: '2024-01-01', Thumbnail: '' }, -]; - describe('SamplesPage', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/tests/unit/Samples/SamplesGrid.test.tsx b/tests/unit/Samples/SamplesGrid.test.tsx index 6cb5332..7b2ab5f 100644 --- a/tests/unit/Samples/SamplesGrid.test.tsx +++ b/tests/unit/Samples/SamplesGrid.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen } from '@testing-library/react'; import { SamplesGrid } from '../../../src/components/Samples/SamplesGrid'; diff --git a/tests/unit/Samples/SamplesGridItem.test.tsx b/tests/unit/Samples/SamplesGridItem.test.tsx index c05b7ef..d133fd8 100644 --- a/tests/unit/Samples/SamplesGridItem.test.tsx +++ b/tests/unit/Samples/SamplesGridItem.test.tsx @@ -1,13 +1,11 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; +import { openFile } from '../../../src/functions/utility'; import { SamplesGridItem } from '../../../src/components/Samples/SamplesGridItem'; jest.mock('../../../src/functions/utility', () => ({ openFile: jest.fn(), })); -const { openFile } = require('../../../src/functions/utility'); - const defaultProps: Samples = { FileName: 'Sample Graph', FilePath: '/path/sample.dyn', diff --git a/tests/unit/SettingsContext.test.tsx b/tests/unit/SettingsContext.test.tsx index 78b5672..ddddb15 100644 --- a/tests/unit/SettingsContext.test.tsx +++ b/tests/unit/SettingsContext.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { SettingsProvider, useSettings } from '../../src/components/SettingsContext'; diff --git a/tests/unit/Sidebar/CustomDropDown.test.tsx b/tests/unit/Sidebar/CustomDropDown.test.tsx index e36a38a..1142468 100644 --- a/tests/unit/Sidebar/CustomDropDown.test.tsx +++ b/tests/unit/Sidebar/CustomDropDown.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { screen, fireEvent } from '@testing-library/react'; import { renderWithIntl } from '../testUtils'; import { CustomDropdown } from '../../../src/components/Sidebar/CustomDropDown'; @@ -108,8 +107,6 @@ describe('CustomDropdown', () => { showDivider={false} /> ); - const spansTrue = cTrue.querySelector('[class*="dropdown-selected"]')?.querySelectorAll('span') ?? []; - const spansFalse = cFalse.querySelector('[class*="dropdown-selected"]')?.querySelectorAll('span') ?? []; // With divider: more spans than without // Note: CSS module class is undefined, so we find dropdown-selected by first div containing spans // Alternative: count all spans in the whole dropdown diff --git a/tests/unit/Sidebar/Sidebar.test.tsx b/tests/unit/Sidebar/Sidebar.test.tsx index 2771065..1412c13 100644 --- a/tests/unit/Sidebar/Sidebar.test.tsx +++ b/tests/unit/Sidebar/Sidebar.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { screen, fireEvent } from '@testing-library/react'; import { renderWithIntl } from '../testUtils'; import { Sidebar } from '../../../src/components/Sidebar/Sidebar'; diff --git a/tests/unit/testUtils.tsx b/tests/unit/testUtils.tsx index 455a66c..1f90ac2 100644 --- a/tests/unit/testUtils.tsx +++ b/tests/unit/testUtils.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import type { ReactElement } from 'react'; import { render } from '@testing-library/react'; import { IntlProvider } from 'react-intl'; import { SettingsProvider } from '../../src/components/SettingsContext'; @@ -6,7 +6,7 @@ import { getMessagesForLocale } from '../../src/localization/localization'; const enMessages = getMessagesForLocale('en'); -export const renderWithIntl = (ui: React.ReactElement, locale: Locale = 'en') => { +export const renderWithIntl = (ui: ReactElement, locale: Locale = 'en') => { const messages = getMessagesForLocale(locale); return render( @@ -15,7 +15,7 @@ export const renderWithIntl = (ui: React.ReactElement, locale: Locale = 'en') => ); }; -export const renderWithProviders = (ui: React.ReactElement) => { +export const renderWithProviders = (ui: ReactElement) => { return render( diff --git a/tsconfig.json b/tsconfig.json index c56f6b3..bfc8d74 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -107,5 +107,5 @@ // "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, // "include": ["src/", "src/**/*.js", "types/**/*"] - "exclude": ["./tests"] + "exclude": ["node_modules"] } From b3c0e6499933063bff8fcf555ad87ad882796796 Mon Sep 17 00:00:00 2001 From: Daniel Velazco Date: Thu, 23 Apr 2026 14:30:41 -0500 Subject: [PATCH 11/12] remove lint fixes --- .eslintrc | 16 +-- package-lock.json | 341 ---------------------------------------------- package.json | 16 ++- tsconfig.json | 2 +- 4 files changed, 14 insertions(+), 361 deletions(-) diff --git a/.eslintrc b/.eslintrc index 31bc479..75aea9d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,4 @@ { - "root": true, "env": { "browser": true, "es2021": true, @@ -8,10 +7,8 @@ }, "extends": [ "eslint:recommended", - "plugin:@typescript-eslint/recommended", "plugin:react/recommended" ], - "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaFeatures": { "jsx": true @@ -19,7 +16,7 @@ "ecmaVersion": "latest", "sourceType": "module" }, - "plugins": ["@typescript-eslint", "react"], + "plugins": ["react"], "settings": { "react": { "version": "detect" @@ -27,15 +24,8 @@ }, "rules": { "indent": ["error", 2], - "semi": "off", - "@typescript-eslint/semi": ["error", "always"], - "quotes": "off", - "@typescript-eslint/quotes": ["error", "single", { "avoidEscape": true }], - "react/react-in-jsx-scope": "off", - "react/prop-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/ban-ts-comment": "off" + "quotes": ["error", "single"], + "semi": ["error", "always"] }, "globals": { "page": true, diff --git a/package-lock.json b/package-lock.json index 10189ae..3adac73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,8 +27,6 @@ "@types/jest": "^29.5.12", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", "babel-loader": "^9.1.3", "css-loader": "^6.8.1", "eslint": "^8.57.0", @@ -3909,238 +3907,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -4676,16 +4442,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -6150,19 +5906,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -7059,36 +6802,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -7630,27 +7343,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -10409,16 +10101,6 @@ "dev": true, "license": "MIT" }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -11099,16 +10781,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -13263,19 +12935,6 @@ "tslib": "2" } }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, "node_modules/ts-jest": { "version": "29.4.9", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", diff --git a/package.json b/package.json index e35822a..cd5489f 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "author": "Autodesk Inc.", "main": "index.tsx", "scripts": { - "lint:check": "eslint --ext .ts,.tsx,.js,.jsx ./src ./tests", - "lint:fix": "eslint --ext .ts,.tsx,.js,.jsx ./src ./tests --fix", - "test:unit": "jest tests/unit", - "test:e2e": "playwright test tests/e2e/", + "lint:check": "eslint src/ tests/", + "lint:fix": "eslint src/ tests/ --fix", + "test:unit": "jest tests/App.test.ts", + "test:e2e": "playwright test tests/e2e.test.ts", "test": "npm run test:unit && npm run test:e2e", "start": "webpack serve --config webpack.config.ts", "build": "webpack --config webpack.config.ts --mode=development", @@ -19,6 +19,12 @@ "license": "npm run license:direct && npm run license:transitive", "version:patch": "npm version patch --no-git-tag-version" }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, "keywords": [ "dynamo", "dynamo-home", @@ -50,8 +56,6 @@ "@types/jest": "^29.5.12", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", "babel-loader": "^9.1.3", "css-loader": "^6.8.1", "eslint": "^8.57.0", diff --git a/tsconfig.json b/tsconfig.json index bfc8d74..c56f6b3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -107,5 +107,5 @@ // "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, // "include": ["src/", "src/**/*.js", "types/**/*"] - "exclude": ["node_modules"] + "exclude": ["./tests"] } From 5bda34ce49876fc82a209627e378fcda4810f3e5 Mon Sep 17 00:00:00 2001 From: Daniel Velazco Date: Fri, 24 Apr 2026 08:15:45 -0500 Subject: [PATCH 12/12] fix broken test scripts --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cd5489f..1c7bdf8 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "scripts": { "lint:check": "eslint src/ tests/", "lint:fix": "eslint src/ tests/ --fix", - "test:unit": "jest tests/App.test.ts", - "test:e2e": "playwright test tests/e2e.test.ts", + "test:unit": "jest tests/unit", + "test:e2e": "playwright test tests/e2e", "test": "npm run test:unit && npm run test:e2e", "start": "webpack serve --config webpack.config.ts", "build": "webpack --config webpack.config.ts --mode=development",