@tscircuit/fake-snippets 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.github/workflows/bundle-size-analysis.yml +2 -2
  2. package/bun-tests/fake-snippets-api/routes/package_releases/create.test.ts +131 -0
  3. package/bun-tests/fake-snippets-api/routes/package_releases/get.test.ts +89 -0
  4. package/bun-tests/fake-snippets-api/routes/package_releases/list.test.ts +157 -0
  5. package/bun-tests/fake-snippets-api/routes/package_releases/update.test.ts +189 -0
  6. package/bun-tests/fake-snippets-api/routes/packages/create.test.ts +26 -0
  7. package/bun-tests/fake-snippets-api/routes/packages/delete.test.ts +100 -0
  8. package/bun-tests/fake-snippets-api/routes/packages/get.test.ts +59 -0
  9. package/bun-tests/fake-snippets-api/routes/packages/list.test.ts +167 -0
  10. package/bun.lock +3514 -0
  11. package/dist/bundle.js +741 -108
  12. package/fake-snippets-api/lib/db/db-client.ts +79 -2
  13. package/fake-snippets-api/lib/db/schema.ts +55 -0
  14. package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +10 -0
  15. package/fake-snippets-api/lib/public-mapping/public-map-package.ts +31 -0
  16. package/fake-snippets-api/lib/with-winter-spec.ts +1 -0
  17. package/fake-snippets-api/routes/api/package_files/download.ts +170 -0
  18. package/fake-snippets-api/routes/api/package_files/get.ts +52 -0
  19. package/fake-snippets-api/routes/api/package_files/list.ts +60 -0
  20. package/fake-snippets-api/routes/api/package_releases/create.ts +83 -0
  21. package/fake-snippets-api/routes/api/package_releases/get.ts +34 -0
  22. package/fake-snippets-api/routes/api/package_releases/list.ts +77 -0
  23. package/fake-snippets-api/routes/api/package_releases/update.ts +96 -0
  24. package/fake-snippets-api/routes/api/packages/create.ts +58 -0
  25. package/fake-snippets-api/routes/api/packages/delete.ts +43 -0
  26. package/fake-snippets-api/routes/api/packages/get.ts +36 -0
  27. package/fake-snippets-api/routes/api/packages/list.ts +56 -0
  28. package/fake-snippets-api/routes/api/snippets/create.ts +5 -3
  29. package/package.json +12 -9
  30. package/playwright-tests/circuit-json-import.spec.ts +133 -0
  31. package/playwright-tests/cmd-click.spec.ts +1 -1
  32. package/playwright-tests/editor-page.spec.ts +1 -1
  33. package/playwright-tests/exampleCircuitJson.ts +498 -0
  34. package/playwright-tests/preview-page.spec.ts +2 -9
  35. package/playwright-tests/snapshots/cmd-click.spec.ts-underlined-imports.png +0 -0
  36. package/playwright-tests/snapshots/editor-page.spec.ts-editor-with-snippet.png +0 -0
  37. package/playwright-tests/snapshots/preview-page.spec.ts-preview-snippet-pcb.png +0 -0
  38. package/src/components/CircuitJsonImportDialog.tsx +186 -0
  39. package/src/components/CodeAndPreview.tsx +60 -2
  40. package/src/components/EditorNav.tsx +23 -2
  41. package/src/components/FootprintDialog.tsx +3 -6
  42. package/src/components/Header2.tsx +7 -0
  43. package/src/components/PrefetchPageLink.tsx +4 -1
  44. package/src/components/SuspenseRunFrame.tsx +16 -0
  45. package/src/components/dialogs/import-snippet-dialog.tsx +12 -8
  46. package/src/hooks/use-debounce.ts +17 -0
  47. package/src/hooks/use-global-store.ts +5 -0
  48. package/src/hooks/use-run-tsx/index.tsx +1 -0
  49. package/src/pages/landing.tsx +2 -2
  50. package/src/pages/preview.tsx +2 -28
  51. package/src/pages/quickstart.tsx +24 -0
  52. package/vite.config.ts +1 -1
  53. package/bun.lockb +0 -0
  54. package/playwright-tests/snapshots/preview-page.spec.ts-preview-snippet-schematic.png +0 -0
@@ -0,0 +1,77 @@
1
+ import { packageReleaseSchema } from "fake-snippets-api/lib/db/schema"
2
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
3
+ import { publicMapPackageRelease } from "fake-snippets-api/lib/public-mapping/public-map-package-release"
4
+ import { z } from "zod"
5
+
6
+ export default withRouteSpec({
7
+ methods: ["POST"],
8
+ auth: "none",
9
+ jsonBody: z
10
+ .object({
11
+ package_id: z.string().optional(),
12
+ package_name: z.string().optional(),
13
+ is_latest: z.boolean().optional(),
14
+ version: z.string().optional(),
15
+ commit_sha: z.string().optional(),
16
+ })
17
+ .refine(({ package_id, package_name }) => {
18
+ if (package_id && package_name) {
19
+ return false
20
+ }
21
+ return true
22
+ }, "package_id and package_name are mutually exclusive")
23
+ .refine(({ package_id, package_name }) => {
24
+ if (!package_id && !package_name) {
25
+ return false
26
+ }
27
+ return true
28
+ }, "package_id or package_name is required"),
29
+ jsonResponse: z.object({
30
+ ok: z.boolean(),
31
+ package_releases: z.array(packageReleaseSchema),
32
+ }),
33
+ })(async (req, ctx) => {
34
+ const { package_id, package_name, is_latest, version, commit_sha } =
35
+ req.jsonBody
36
+
37
+ if (!package_id && !package_name && !is_latest && !version && !commit_sha) {
38
+ return ctx.error(400, {
39
+ error_code: "invalid_query",
40
+ message:
41
+ "At least one of package_id, package_name, is_latest, version or commit_sha is required",
42
+ })
43
+ }
44
+
45
+ let releases = ctx.db.packageReleases
46
+
47
+ // Apply filters
48
+ if (package_id) {
49
+ releases = releases.filter((pr) => pr.package_id === package_id)
50
+ }
51
+
52
+ if (package_name) {
53
+ const pkg = ctx.db.packages.find((p) => p.name === package_name)
54
+ if (pkg) {
55
+ releases = releases.filter((pr) => pr.package_id === pkg.package_id)
56
+ } else {
57
+ releases = []
58
+ }
59
+ }
60
+
61
+ if (is_latest !== undefined) {
62
+ releases = releases.filter((pr) => pr.is_latest === is_latest)
63
+ }
64
+
65
+ if (version) {
66
+ releases = releases.filter((pr) => pr.version === version)
67
+ }
68
+
69
+ if (commit_sha) {
70
+ releases = releases.filter((pr) => pr.commit_sha === commit_sha)
71
+ }
72
+
73
+ return ctx.json({
74
+ ok: true,
75
+ package_releases: releases.map((pr) => publicMapPackageRelease(pr)),
76
+ })
77
+ })
@@ -0,0 +1,96 @@
1
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2
+ import { z } from "zod"
3
+
4
+ export default withRouteSpec({
5
+ methods: ["POST"],
6
+ auth: "none",
7
+ jsonBody: z.object({
8
+ package_release_id: z.string().optional(),
9
+ package_name_with_version: z.string().optional(),
10
+ is_locked: z.boolean().optional(),
11
+ is_latest: z.boolean().optional(),
12
+ license: z.string().optional(),
13
+ }),
14
+ jsonResponse: z.object({
15
+ ok: z.boolean(),
16
+ }),
17
+ })(async (req, ctx) => {
18
+ const {
19
+ package_release_id,
20
+ package_name_with_version,
21
+ is_locked,
22
+ is_latest,
23
+ license,
24
+ } = req.jsonBody
25
+ let releaseId = package_release_id
26
+
27
+ // Handle package_name_with_version lookup
28
+ if (!releaseId && package_name_with_version) {
29
+ const [packageName, version] = package_name_with_version.split("@")
30
+ const pkg = ctx.db.packages.find((p) => p.name === packageName)
31
+ if (pkg) {
32
+ const release = ctx.db.packageReleases.find(
33
+ (pr) => pr.package_id === pkg.package_id && pr.version === version,
34
+ )
35
+ if (release) {
36
+ releaseId = release.package_release_id
37
+ }
38
+ }
39
+ }
40
+
41
+ if (!releaseId) {
42
+ return ctx.error(404, {
43
+ error_code: "package_release_not_found",
44
+ message: "Package release not found",
45
+ })
46
+ }
47
+
48
+ const delta = { is_locked, is_latest, license }
49
+ if (
50
+ Object.keys(delta).filter(
51
+ (k) => delta[k as keyof typeof delta] !== undefined,
52
+ ).length === 0
53
+ ) {
54
+ return ctx.error(400, {
55
+ error_code: "no_fields_provided",
56
+ message: "No fields provided to update",
57
+ })
58
+ }
59
+
60
+ const release = ctx.db.packageReleases.find(
61
+ (pr) => pr.package_release_id === releaseId,
62
+ )
63
+ if (!release) {
64
+ return ctx.error(404, {
65
+ error_code: "package_release_not_found",
66
+ message: "Package release not found",
67
+ })
68
+ }
69
+
70
+ // Handle is_latest updates
71
+ if (is_latest !== undefined && is_latest) {
72
+ ctx.db.packageReleases
73
+ .filter(
74
+ (pr) =>
75
+ pr.package_id === release.package_id &&
76
+ pr.package_release_id !== releaseId &&
77
+ pr.is_latest,
78
+ )
79
+ .forEach((pr) => {
80
+ pr.is_latest = false
81
+ })
82
+ }
83
+
84
+ // Update the release
85
+ Object.assign(release, {
86
+ ...(is_locked !== undefined && { is_locked }),
87
+ ...(is_latest !== undefined && { is_latest }),
88
+ ...(license !== undefined && { license }),
89
+ })
90
+
91
+ ctx.db.updatePackageRelease(release)
92
+
93
+ return ctx.json({
94
+ ok: true,
95
+ })
96
+ })
@@ -0,0 +1,58 @@
1
+ import { packageSchema } from "fake-snippets-api/lib/db/schema"
2
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
3
+ import { publicMapPackage } from "fake-snippets-api/lib/public-mapping/public-map-package"
4
+ import { z } from "zod"
5
+
6
+ export default withRouteSpec({
7
+ methods: ["POST"],
8
+ auth: "session",
9
+ jsonBody: z.object({
10
+ name: z
11
+ .string()
12
+ .regex(
13
+ /^[@a-zA-Z0-9-_\/]+$/,
14
+ "Package name can only contain letters, numbers, hyphens, underscores, and forward slashes",
15
+ )
16
+ .transform((name) => name.replace(/^@/, "")),
17
+ description: z.string().optional(),
18
+ }),
19
+ jsonResponse: z.object({
20
+ package: packageSchema.optional(),
21
+ }),
22
+ })(async (req, ctx) => {
23
+ const { name, description } = req.jsonBody
24
+
25
+ const existingPackage = ctx.db.packages.find((pkg) => pkg.name === name)
26
+
27
+ if (existingPackage) {
28
+ throw ctx.error(400, {
29
+ error_code: "package_already_exists",
30
+ message: "A package with this name already exists",
31
+ })
32
+ }
33
+
34
+ const newPackage = ctx.db.addPackage({
35
+ name,
36
+ description: description ?? null,
37
+ creator_account_id: ctx.auth.account_id,
38
+ owner_org_id: ctx.auth.personal_org_id,
39
+ owner_github_username: ctx.auth.github_username,
40
+ latest_package_release_id: null,
41
+ latest_version: null,
42
+ license: null,
43
+ is_source_from_github: false,
44
+ created_at: new Date().toISOString(),
45
+ updated_at: new Date().toISOString(),
46
+ unscoped_name: name,
47
+ star_count: 0,
48
+ ai_description: name,
49
+ })
50
+
51
+ if (!newPackage) {
52
+ throw new Error("Failed to create package")
53
+ }
54
+
55
+ return ctx.json({
56
+ package: publicMapPackage(newPackage),
57
+ })
58
+ })
@@ -0,0 +1,43 @@
1
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2
+ import { z } from "zod"
3
+
4
+ export default withRouteSpec({
5
+ methods: ["POST"],
6
+ auth: "session",
7
+ jsonBody: z.object({
8
+ package_id: z.string(),
9
+ }),
10
+ jsonResponse: z.object({
11
+ ok: z.boolean(),
12
+ }),
13
+ })(async (req, ctx) => {
14
+ const { package_id } = req.jsonBody
15
+
16
+ const packageIndex = ctx.db.packages.findIndex(
17
+ (p) => p.package_id === package_id,
18
+ )
19
+
20
+ if (packageIndex === -1) {
21
+ return ctx.error(404, {
22
+ error_code: "package_not_found",
23
+ message: "Package not found",
24
+ })
25
+ }
26
+
27
+ const pkg = ctx.db.packages[packageIndex]
28
+
29
+ console.log("pkg", pkg.owner_org_id, ctx.auth.personal_org_id)
30
+
31
+ if (pkg.owner_org_id !== ctx.auth.personal_org_id) {
32
+ return ctx.error(403, {
33
+ error_code: "forbidden",
34
+ message: "You don't have permission to delete this package",
35
+ })
36
+ }
37
+
38
+ ctx.db.packages.splice(packageIndex, 1)
39
+
40
+ return ctx.json({
41
+ ok: true,
42
+ })
43
+ })
@@ -0,0 +1,36 @@
1
+ import { packageSchema } from "fake-snippets-api/lib/db/schema"
2
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
3
+ import { publicMapPackage } from "fake-snippets-api/lib/public-mapping/public-map-package"
4
+ import { z } from "zod"
5
+
6
+ export default withRouteSpec({
7
+ methods: ["GET", "POST"],
8
+ auth: "optional_session",
9
+ commonParams: z.object({
10
+ package_id: z.string().optional(),
11
+ name: z.string().optional(),
12
+ }),
13
+ jsonBody: z.any().optional(),
14
+ jsonResponse: z.object({
15
+ ok: z.boolean(),
16
+ package: packageSchema.optional(),
17
+ }),
18
+ })(async (req, ctx) => {
19
+ const { package_id, name } = req.commonParams
20
+
21
+ const foundPackage =
22
+ (package_id && ctx.db.getPackageById(package_id)) ||
23
+ ctx.db.packages.find((p) => p.name === name)
24
+
25
+ if (!foundPackage) {
26
+ return ctx.error(404, {
27
+ error_code: "package_not_found",
28
+ message: `Package not found (searched using ${JSON.stringify(req.commonParams)})`,
29
+ })
30
+ }
31
+
32
+ return ctx.json({
33
+ ok: true,
34
+ package: publicMapPackage(foundPackage),
35
+ })
36
+ })
@@ -0,0 +1,56 @@
1
+ import { packageSchema } from "fake-snippets-api/lib/db/schema"
2
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
3
+ import { z } from "zod"
4
+
5
+ export default withRouteSpec({
6
+ methods: ["GET", "POST"],
7
+ auth: "optional_session",
8
+ commonParams: z.object({
9
+ creator_account_id: z.string().optional(),
10
+ owner_github_username: z.string().optional(),
11
+ is_writable: z.boolean().optional(),
12
+ name: z.string().optional(),
13
+ }),
14
+ jsonResponse: z.object({
15
+ ok: z.boolean(),
16
+ packages: z.array(packageSchema),
17
+ }),
18
+ })(async (req, ctx) => {
19
+ const { creator_account_id, owner_github_username, name, is_writable } =
20
+ req.commonParams
21
+
22
+ const auth = "auth" in ctx && ctx.auth ? ctx.auth : null
23
+
24
+ if (!auth && !is_writable && !creator_account_id && !owner_github_username) {
25
+ return ctx.error(400, {
26
+ error_code: "invalid_request",
27
+ message: "You must provide some filtering parameters or be logged in",
28
+ })
29
+ }
30
+
31
+ let packages = ctx.db.packages
32
+
33
+ // Apply filters
34
+ if (creator_account_id) {
35
+ packages = packages.filter(
36
+ (p) => p.creator_account_id === creator_account_id,
37
+ )
38
+ }
39
+ if (owner_github_username) {
40
+ packages = packages.filter(
41
+ (p) => p.owner_github_username === owner_github_username,
42
+ )
43
+ }
44
+ if (name) {
45
+ packages = packages.filter((p) => p.name === name)
46
+ }
47
+
48
+ if (is_writable && auth) {
49
+ packages = packages.filter((p) => p.owner_org_id === auth.personal_org_id)
50
+ }
51
+
52
+ return ctx.json({
53
+ ok: true,
54
+ packages,
55
+ })
56
+ })
@@ -46,8 +46,10 @@ export default withRouteSpec({
46
46
  })
47
47
  }
48
48
 
49
- const newSnippet: z.input<typeof snippetSchema> = {
50
- snippet_id: `snippet_${ctx.db.idCounter + 1}`,
49
+ let newSnippet: Omit<
50
+ z.input<typeof snippetSchema>,
51
+ "snippet_id" | "package_release_id"
52
+ > = {
51
53
  name: `${ctx.auth.github_username}/${unscoped_name}`,
52
54
  unscoped_name,
53
55
  owner_name: ctx.auth.github_username,
@@ -62,7 +64,7 @@ export default withRouteSpec({
62
64
  }
63
65
 
64
66
  try {
65
- ctx.db.addSnippet(newSnippet)
67
+ newSnippet = ctx.db.addSnippet(newSnippet)
66
68
  } catch (error) {
67
69
  return ctx.error(500, {
68
70
  error_code: "snippet_creation_failed",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fake-snippets",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -61,13 +61,13 @@
61
61
  "@radix-ui/react-toggle": "^1.1.0",
62
62
  "@radix-ui/react-toggle-group": "^1.1.0",
63
63
  "@radix-ui/react-tooltip": "^1.1.2",
64
- "@tscircuit/3d-viewer": "^0.0.96",
65
- "@tscircuit/footprinter": "^0.0.99",
64
+ "@tscircuit/3d-viewer": "^0.0.113",
65
+ "@tscircuit/footprinter": "^0.0.102",
66
66
  "@tscircuit/layout": "^0.0.29",
67
67
  "@tscircuit/math-utils": "^0.0.10",
68
68
  "@tscircuit/mm": "^0.0.8",
69
69
  "@tscircuit/pcb-viewer": "^1.11.12",
70
- "@tscircuit/props": "^0.0.129",
70
+ "@tscircuit/props": "^0.0.138",
71
71
  "@tscircuit/schematic-viewer": "^1.4.3",
72
72
  "@types/file-saver": "^2.0.7",
73
73
  "@types/ms": "^0.7.34",
@@ -76,18 +76,19 @@
76
76
  "@valtown/codemirror-ts": "^2.2.0",
77
77
  "@vercel/analytics": "^1.4.1",
78
78
  "change-case": "^5.4.4",
79
- "circuit-json": "^0.0.130",
79
+ "circuit-json": "^0.0.135",
80
80
  "circuit-json-to-bom-csv": "^0.0.6",
81
81
  "circuit-json-to-gerber": "^0.0.16",
82
82
  "circuit-json-to-pnp-csv": "^0.0.6",
83
- "circuit-json-to-readable-netlist": "^0.0.5",
83
+ "circuit-json-to-tscircuit": "^0.0.4",
84
+ "circuit-json-to-readable-netlist": "^0.0.7",
84
85
  "class-variance-authority": "^0.7.0",
85
86
  "clsx": "^2.1.1",
86
87
  "cmdk": "^1.0.4",
87
88
  "codemirror": "^6.0.1",
88
89
  "country-list": "^2.3.0",
89
90
  "date-fns": "^4.1.0",
90
- "dsn-converter": "^0.0.57",
91
+ "dsn-converter": "^0.0.60",
91
92
  "easyeda": "^0.0.62",
92
93
  "embla-carousel-react": "^8.3.0",
93
94
  "extract-codefence": "^0.0.4",
@@ -129,11 +130,13 @@
129
130
  "@babel/standalone": "^7.26.2",
130
131
  "@biomejs/biome": "^1.9.2",
131
132
  "@playwright/test": "^1.48.0",
132
- "@tscircuit/core": "^0.0.269",
133
- "@tscircuit/prompt-benchmarks": "^0.0.20",
133
+ "@tscircuit/core": "^0.0.296",
134
+ "@tscircuit/prompt-benchmarks": "^0.0.28",
135
+ "@tscircuit/runframe": "^0.0.139",
134
136
  "@types/babel__standalone": "^7.1.7",
135
137
  "@types/bun": "^1.1.10",
136
138
  "@types/country-list": "^2.1.4",
139
+ "@types/node": "^22.13.0",
137
140
  "@types/prismjs": "^1.26.4",
138
141
  "@types/react": "^18.3.9",
139
142
  "@types/react-dom": "^18.3.0",
@@ -0,0 +1,133 @@
1
+ import { test, expect } from "@playwright/test"
2
+ import { exampleCircuitJson } from "./exampleCircuitJson"
3
+
4
+ async function loginToSite(page) {
5
+ const loginButton = page.getByRole("button", { name: "Log in" })
6
+ if (await loginButton.isVisible()) {
7
+ await loginButton.click()
8
+ await page.waitForLoadState("networkidle")
9
+ }
10
+ }
11
+
12
+ test.beforeEach(async ({ page }) => {
13
+ await page.goto("http://127.0.0.1:5177/quickstart")
14
+ await page.waitForTimeout(3000)
15
+ await loginToSite(page).catch(() => {})
16
+ })
17
+
18
+ test("should open and close the Circuit Json Import Dialog", async ({
19
+ page,
20
+ }) => {
21
+ const importButton = page.locator('button:has-text("Import Circuit JSON")')
22
+ await importButton.click()
23
+
24
+ const dialog = page.getByRole("dialog")
25
+ await expect(dialog).toBeVisible()
26
+
27
+ const closeButton = dialog.getByRole("button", { name: "Close" })
28
+ await closeButton.click()
29
+
30
+ await expect(dialog).not.toBeVisible()
31
+ })
32
+
33
+ test("should handle valid Circuit JSON input", async ({ page }) => {
34
+ const importButton = page.getByRole("button", { name: "Import Circuit JSON" })
35
+ await importButton.click()
36
+ const textarea = page.locator(
37
+ 'textarea[placeholder="Paste the Circuit JSON."]',
38
+ )
39
+ await textarea.fill(JSON.stringify(exampleCircuitJson))
40
+
41
+ const importDialogButton = page.getByRole("button", { name: "Import" })
42
+ await importDialogButton.click()
43
+
44
+ const successToast = page.locator(
45
+ 'div.text-sm.font-semibold:has-text("Import Successful")',
46
+ )
47
+ await successToast.waitFor({ state: "visible", timeout: 5000 })
48
+ await expect(successToast).toBeVisible()
49
+ })
50
+
51
+ test("should handle valid Circuit JSON file upload", async ({ page }) => {
52
+ const importButton = page.locator('button:has-text("Import Circuit JSON")')
53
+ await importButton.click()
54
+
55
+ const fileInput = page.locator('input[type="file"]')
56
+
57
+ await fileInput.setInputFiles({
58
+ name: "circuit.json",
59
+ mimeType: "application/json",
60
+ // @ts-expect-error didnt add node types to tsconfig
61
+ buffer: Buffer.from(JSON.stringify(exampleCircuitJson)),
62
+ })
63
+
64
+ const importDialogButton = page.getByRole("button", { name: "Import" })
65
+ await importDialogButton.click()
66
+ const successToast = page.locator(
67
+ 'div.text-sm.font-semibold:has-text("Import Successful")',
68
+ )
69
+ await successToast.waitFor({ state: "visible", timeout: 5000 })
70
+ await expect(successToast).toBeVisible()
71
+ })
72
+
73
+ test("should handle invalid Circuit JSON input", async ({ page }) => {
74
+ const importButton = page.locator('button:has-text("Import Circuit JSON")')
75
+ await importButton.click()
76
+
77
+ const textarea = page.locator(
78
+ 'textarea[placeholder="Paste the Circuit JSON."]',
79
+ )
80
+ await textarea.fill("invalid json content")
81
+
82
+ const importDialogButton = page.getByRole("button", { name: "Import" })
83
+ await importDialogButton.click()
84
+
85
+ const errorToast = page.locator(
86
+ 'div.text-sm.font-semibold:has-text("Invalid Input")',
87
+ )
88
+ await errorToast.waitFor({ state: "visible", timeout: 5000 })
89
+ await expect(errorToast).toBeVisible()
90
+ })
91
+
92
+ test("should handle invalid Circuit JSON file upload", async ({ page }) => {
93
+ const importButton = page.locator('button:has-text("Import Circuit JSON")')
94
+ await importButton.click()
95
+
96
+ const fileInput = page.locator('input[type="file"]')
97
+ await fileInput.setInputFiles({
98
+ name: "circuit.json",
99
+ mimeType: "application/json",
100
+ // @ts-expect-error didnt add node types to tsconfig
101
+ buffer: Buffer.from(JSON.stringify({})),
102
+ })
103
+
104
+ const importDialogButton = page.getByRole("button", { name: "Import" })
105
+ await importDialogButton.click()
106
+
107
+ const errorToast = page.locator(
108
+ 'div.text-sm.font-semibold:has-text("Import Failed")',
109
+ )
110
+ await errorToast.waitFor({ state: "visible", timeout: 5000 })
111
+ await expect(errorToast).toBeVisible()
112
+ })
113
+
114
+ test("should handle non-JSON file upload", async ({ page }) => {
115
+ const importButton = page.locator('button:has-text("Import Circuit JSON")')
116
+ await importButton.click()
117
+
118
+ const fileInput = page.locator('input[type="file"]')
119
+ await fileInput.setInputFiles({
120
+ name: "circuit.txt",
121
+ mimeType: "application/text",
122
+ // @ts-expect-error didnt add node types to tsconfig
123
+ buffer: Buffer.from(""),
124
+ })
125
+
126
+ const importDialogButton = page.getByRole("button", { name: "Import" })
127
+ await importDialogButton.click()
128
+
129
+ const errorToast = page.locator(
130
+ 'div.pb-4 > p:has-text("Please select a valid JSON file.")',
131
+ )
132
+ await expect(errorToast).toBeVisible()
133
+ })
@@ -3,7 +3,7 @@ import { test, expect } from "@playwright/test"
3
3
  test("Editor handles imports with underlining and cmd+click", async ({
4
4
  page,
5
5
  }) => {
6
- await page.goto("http://127.0.0.1:5177/editor?snippet_id=snippet_5")
6
+ await page.goto("http://127.0.0.1:5177/editor?snippet_id=snippet_3")
7
7
 
8
8
  await page.waitForLoadState("networkidle")
9
9
 
@@ -8,7 +8,7 @@ test("Editor loads snippet correctly", async ({ page }) => {
8
8
  await page.waitForLoadState("networkidle")
9
9
 
10
10
  // Check for specific text that should be present
11
- await expect(page.getByText("SquareWaveModule")).toBeVisible()
11
+ await expect(page.getByText("A555Timer", { exact: true })).toBeVisible()
12
12
 
13
13
  // Take a snapshot
14
14
  await expect(page).toHaveScreenshot("editor-with-snippet.png")