Skip to content

test(ui): cover fontAssets service (API, hooks, FontFace loading)#439

Merged
streamer45 merged 2 commits into
mainfrom
devin/1778924866-test-ui-fontAssets
May 16, 2026
Merged

test(ui): cover fontAssets service (API, hooks, FontFace loading)#439
streamer45 merged 2 commits into
mainfrom
devin/1778924866-test-ui-fontAssets

Conversation

@staging-devin-ai-integration
Copy link
Copy Markdown
Contributor

@staging-devin-ai-integration staging-devin-ai-integration Bot commented May 16, 2026

Summary

Phase 2 coverage sprint — expand ui/src/services/fontAssets.test.ts beyond the existing five fontFamilyForAsset cases to cover the rest of the public surface of ui/src/services/fontAssets.ts (previously 59 uncovered lines, 7.8% covered, no #[cfg(test)]-equivalent block for the API / hooks / loadFontAssets).

Behaviors covered (file → describe block):

  • fontFamilyForAsset — derives sk-… family from a path, strips only the final extension, preserves -Bold suffixes, handles bare filenames, paths with no extension, and multi-dot filenames.
  • listFontAssets — GETs /api/v1/assets/fonts with credentials: 'include', returns the parsed JSON array, throws including statusText on 4xx, and surfaces network errors (fetch rejection).
  • uploadFontAsset — POSTs a FormData with the file under the file key to the correct URL, throws including the server error text on 413, on 409 re-fetches the list and reuses the asset whose id matches the sanitized filename (e.g. My Font.ttfMy_Font.ttf), and on 409 with no match throws Font asset already exists: ….
  • deleteFontAsset — DELETEs /api/v1/assets/fonts/<encoded id> (asserts percent-encoding via My Font.ttfMy%20Font.ttf) with credentials: 'include', and throws including the server error text on 404.
  • loadFontAssets — registers each asset via the FontFace constructor with the right sk-… family, the right url(/api/v1/assets/fonts/file/<scope>/<filename>) source (with percent-encoding of spaces in the path), and the right weight ('400' vs '700' for -Bold ids); calls document.fonts.add(face) after load() resolves; swallows per-asset load() failures via Promise.allSettled so the overall load still resolves; and caches by asset.path so repeat calls do not re-register the same path.
  • useFontAssets / useUploadFontAsset / useDeleteFontAssetuseFontAssets resolves to the listed assets, surfaces fetch errors as an error state, and stays idle (no fetch call) when enabled=false; both mutation hooks call their underlying function and invalidate the ['fontAssets'] query key on success (asserted via a vi.spyOn(queryClient, 'invalidateQueries')).

Style mirrors the existing service tests (converter.test.ts, marketplace.test.ts, permissions.test.ts): vi.mock('./base', …) forwarding fetchApi to global.fetch = vi.fn(), plus a small mockResponse({...}) helper for the ok/status/statusText/json/text shape. loadFontAssets is exercised with a FontFace class stub + Object.defineProperty(document, 'fonts', …) (happy-dom has no native FontFace / document.fonts).

Files inspected for style:

  • ui/src/services/converter.test.ts
  • ui/src/services/marketplace.test.ts
  • ui/src/services/permissions.test.ts
  • ui/src/services/base.ts
  • ui/src/hooks/useEdgeAlertSubscription.test.ts (renderHook + @testing-library/react patterns)
  • ui/src/test/setup.ts

Commands run:

  • just lint-ui — passes (only pre-existing warnings in AssetLibrary.tsx, ConfigurableNode.tsx, InspectorPane.tsx, DesignView.tsx, StreamView.tsx; no new warnings or errors).
  • bun run test:run src/services/fontAssets.test.ts — 24/24 pass.
  • bun run test:run (full UI suite) — 662/662 pass across 44 files.

Review & Testing Checklist for Human

Risk: green — tests-only PR, no production code touched.

  • bun run test:run src/services/fontAssets.test.ts passes locally (24 tests).
  • just lint-ui introduces no new warnings.
  • The 409-conflict assertion matches your expectations for the upload reuse path (file.name.replace(/[^a-zA-Z0-9._-]/g, '_') must match an existing asset.id).

Notes

  • No production code changed.
  • fontServeUrl is exercised indirectly through loadFontAssets (it is module-private and not exported); the assertions on the FontFace source argument verify the URL shape and percent-encoding without reaching into the private symbol.
  • Module-level loadedFonts: Set<string> is shared across tests; each loadFontAssets test uses a unique path prefix (load1/, load2/, load3/, load4/) so test order does not matter.

Follow-ups / observations (no real bugs found):

  • loadFontFace uses asset.id.includes('-Bold') || asset.id.includes('Bold') — the second branch is a strict superset of the first, so the first is dead. Behaviour is unchanged (anything containing Bold is bold), so I did not test the two branches separately; flagging here for the coordinator in case it's worth simplifying.
  • Hooks pass an enabled boolean; useFontAssets(false) correctly leaves fetchStatus === 'idle' and never calls fetch. No bug.

Link to Devin session: https://staging.itsdev.in/sessions/f21c1037d23e40bea6dc08bfcdca2b30
Requested by: @streamer45


Devin Review

Status Commit
🕐 Outdated b1c7fb9 (HEAD is 36a326a)

Run Devin Review

Open in Devin Review (Staging)

Expand ui/src/services/fontAssets.test.ts beyond the existing
fontFamilyForAsset checks to cover the rest of the public surface:

- listFontAssets / uploadFontAsset / deleteFontAsset: URL + method +
  credentials + body assertions, 4xx error propagation, network reject,
  409 conflict reuse-by-sanitized-id path, and 409-without-match throw.
- loadFontAssets: FontFace family/URL/weight wiring, percent-encoding
  of paths with spaces, per-asset failures swallowed via allSettled,
  and per-path caching across repeat calls.
- React Query hooks: useFontAssets loading/success/error/disabled, and
  useUploadFontAsset / useDeleteFontAsset invalidate ['fontAssets'] on
  success.

Mirrors the vi.mock('./base') + global.fetch = vi.fn() style already
used by converter.test.ts / marketplace.test.ts / permissions.test.ts.

Phase 2 coverage sprint.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@staging-devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@codecov
Copy link
Copy Markdown

codecov Bot commented May 16, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.05%. Comparing base (90d476c) to head (36a326a).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #439      +/-   ##
==========================================
+ Coverage   62.90%   63.05%   +0.14%     
==========================================
  Files         215      215              
  Lines       55435    55435              
  Branches     1597     1597              
==========================================
+ Hits        34874    34955      +81     
+ Misses      20555    20474      -81     
  Partials        6        6              
Flag Coverage Δ
backend 62.79% <ø> (+0.02%) ⬆️
ui 65.52% <ø> (+1.30%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
core 84.29% <ø> (ø)
engine 75.24% <ø> (ø)
api 84.73% <ø> (ø)
nodes 67.09% <ø> (+0.05%) ⬆️
server 50.64% <ø> (ø)
plugin-native 70.93% <ø> (ø)
plugin-wasm 6.37% <ø> (ø)
ui-services 66.65% <ø> (+2.44%) ⬆️
ui-components 60.49% <ø> (ø)
see 2 files with indirect coverage changes
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

Open in Devin Review (Staging)
Debug

Playground

Comment thread ui/src/services/fontAssets.test.ts Outdated
});
});

// ── loadFontAssets ──────────────────────────────────────────────────────────
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Added section-divider comment violates repository comment rules

AGENTS.md explicitly forbids section-divider comments and says to use blank lines or code structure instead. The added loadFontAssets section header is a pure divider/comment label; the surrounding describe('loadFontAssets', ...) and test names already provide the structure, so this violates the repository's mandatory comment guidelines.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 36a326a. Removed both // ── loadFontAssets ── and // ── React Query hooks ── dividers; the surrounding describe blocks already provide the structure.

Comment thread ui/src/services/fontAssets.test.ts Outdated
});
});

// ── React Query hooks ───────────────────────────────────────────────────────
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Added React Query section divider violates repository comment rules

AGENTS.md explicitly forbids section-divider comments and says to use blank lines or code structure instead. The added React Query hooks header is a pure divider/comment label; the following wrapper and describe('useFontAssets / useUploadFontAsset / useDeleteFontAsset', ...) already provide the structure, so this violates the repository's mandatory comment guidelines.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +179 to +197
it('on 409 conflict, refetches the list and reuses the asset matching the sanitized filename', async () => {
const existing: FontAsset = {
...SYSTEM_ASSET,
id: 'My_Font.ttf',
name: 'My Font',
path: 'samples/fonts/user/My_Font.ttf',
is_system: false,
};
fetchMock()
.mockResolvedValueOnce(mockResponse({ ok: false, status: 409 }))
.mockResolvedValueOnce(mockResponse({ ok: true, status: 200, json: [existing] }));

const file = new File(['data'], 'My Font.ttf', { type: 'font/ttf' });
const result = await uploadFontAsset(file);

expect(result).toEqual(existing);
expect(fetchMock()).toHaveBeenCalledTimes(2);
expect(fetchMock().mock.calls[1][0]).toBe('http://localhost:4545/api/v1/assets/fonts');
});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: 409 conflict test documents intentional client-side reuse behavior

The new test around upload conflicts verifies an existing non-obvious behavior in uploadFontAsset: a 409 response causes the client to refetch listFontAssets() and return the asset whose id equals the locally sanitized filename (ui/src/services/fontAssets.ts:92-100). I did not flag this as a bug because the test matches the implementation and the affected UI caller treats the mutation result as a successful upload before invalidating ['fontAssets'], so the behavior appears intentional rather than an accidental swallowing of conflicts.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — the 409 reuse-by-sanitized-id path is intentional behavior of uploadFontAsset (fontAssets.ts:92-100) and the test pins the public contract. Not a bug; just locking it down so a future refactor can't silently break the reuse fallback.

Comment on lines +343 to +357
it('caches loaded fonts so repeated calls for the same path do not re-register', async () => {
const load = vi.fn().mockResolvedValue(undefined);
installFontFaceStub(load);

const asset: FontAsset = {
...SYSTEM_ASSET,
id: 'load4-Once.ttf',
path: 'load4/system/Once.ttf',
};
await loadFontAssets([asset]);
expect(load).toHaveBeenCalledTimes(1);

await loadFontAssets([asset]);
expect(load).toHaveBeenCalledTimes(1);
});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: FontFace cache test relies on module-level state persisting across tests

The new caching test exercises the module-level loadedFonts set in fontAssets.ts; that set is not exported or reset between tests, so the test suite avoids cross-test interference by using unique synthetic path prefixes (load1, load2, etc.) for each loadFontAssets case. This is worth preserving if more tests are added, because reusing a previously loaded path would skip FontFace registration and make assertions depend on test order.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — loadedFonts is a module-private Set<string> with no reset hook, so the suite uses load1/load4/ path prefixes to keep each loadFontAssets test independent of order. Called this out in the PR body's "Notes" section so future additions follow the same convention.

Comment on lines +371 to +379
beforeEach(() => {
global.fetch = vi.fn() as never;
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: 0 },
mutations: { retry: false },
},
});
wrapper = makeWrapper(queryClient);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: React Query hook tests use a real QueryClient with retries disabled

The hook tests create a dedicated QueryClient with queries.retry and mutations.retry disabled before rendering hooks, which is important because otherwise failed fetch assertions could observe retry-driven extra calls or delayed error states. This matches the service hooks' actual query key and invalidation contract in fontAssets.ts:146-179, so I did not identify a cache-key or invalidation bug in the added tests.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Devin Review flagged the // \u2500\u2500 loadFontAssets \u2500\u2500 and // \u2500\u2500 React
Query hooks \u2500\u2500 lines as violations of the repo comment guidelines
(AGENTS.md: 'Section dividers \u2014 `// --- Public Modules ---`, `// State`,
`// Handlers`. Use blank lines or code structure instead.'). The
surrounding describe blocks and test names already provide structure.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@streamer45 streamer45 merged commit bc2a891 into main May 16, 2026
25 checks passed
@streamer45 streamer45 deleted the devin/1778924866-test-ui-fontAssets branch May 16, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants