Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@
## 2026-07-05 - content-visibility와 scrollbar jumping λ°©μ§€
**Learning:** κΈ΄ 단일 νŽ˜μ΄μ§€(static site)μ—μ„œ `content-visibility: auto`λ₯Ό μ‚¬μš©ν•˜μ—¬ μ˜€ν”„μŠ€ν¬λ¦° μ„Ήμ…˜μ˜ λ Œλ”λ§μ„ μ΅œμ ν™”ν•  λ•Œ, `contain-intrinsic-size`λ₯Ό ν•¨κ»˜ μ§€μ •ν•˜μ§€ μ•ŠμœΌλ©΄ μŠ€ν¬λ‘€λ°”κ°€ νŠ€κ±°λ‚˜ λ ˆμ΄μ•„μ›ƒ μ‹œν”„νŠΈκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.
**Action:** 항상 길이 기반 폴백(예: `contain-intrinsic-size: 600px;`)을 μ„ ν–‰ν•˜κ³ , λΈŒλΌμš°μ €κ°€ μ‹€μ œ 높이λ₯Ό κΈ°μ–΅ν•  수 μžˆλ„λ‘ `auto` ν‚€μ›Œλ“œλ₯Ό ν¬ν•¨ν•œ 속성을 μ„€μ •ν•©λ‹ˆλ‹€. μ„Ήμ…˜λ³„ μ‹€μ œ 높이에 맞좰 크기λ₯Ό μ‘°μ •ν•©λ‹ˆλ‹€.
## 2024-05-24 - 이미지 λ””μ½”λ”© μ΅œμ ν™”λ₯Ό ν†΅ν•œ 메인 μŠ€λ ˆλ“œ 차단 λ°©μ§€
**Learning:** 이미지 λ‘œλ“œ μ‹œ 기본적으둜 메인 μŠ€λ ˆλ“œμ—μ„œ 디코딩을 μˆ˜ν–‰ν•˜κΈ° λ•Œλ¬Έμ— 메인 μŠ€λ ˆλ“œ 차단 및 UI jankκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.
**Action:** μ˜€ν”„μŠ€ν¬λ¦° μ΄λ―Έμ§€λ‚˜ λ ˆμ΄μ§€ λ‘œλ“œ 이미지 등에 `decoding="async"` 속성을 μΆ”κ°€ν•˜μ—¬ λ°±κ·ΈλΌμš΄λ“œ μŠ€λ ˆλ“œμ—μ„œ λ””μ½”λ”© μž‘μ—…μ„ μ²˜λ¦¬ν•˜λ„λ‘ ν•¨μœΌλ‘œμ¨ 초기 νŽ˜μ΄μ§€ λ‘œλ“œ μ„±λŠ₯을 κ°œμ„ ν•©λ‹ˆλ‹€.
## 2024-05-24 - LCP μ΅œμ ν™” 및 SVG 이미지 처리 νŒ¨ν„΄
**Learning:** LCP(κ°€μž₯ 큰 μ½˜ν…μΈ  ν’€ 페인트) λŒ€μƒ 이미지에 `decoding="async"` 속성을 λΆ€μ—¬ν•˜λ©΄ λΈŒλΌμš°μ €κ°€ 디코딩을 λ°±κ·ΈλΌμš΄λ“œλ‘œ λ„˜κΈ°κ²Œ λ˜μ–΄ 초기 νŽ˜μΈνŒ…μ΄ μ§€μ—°λ˜λŠ” μ•ˆν‹°νŒ¨ν„΄μ΄ 될 수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ, SVG μ΄λ―Έμ§€λŠ” λž˜μŠ€ν„° μ΄λ―Έμ§€μ²˜λŸΌ λ””μ½”λ”©λ˜λŠ” 것이 μ•„λ‹ˆλΌ νŒŒμ‹±λ˜λ―€λ‘œ `decoding="async"`의 νš¨κ³Όκ°€ 거의 μ—†μŒμ„ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.
**Action:** LCP μ΄λ―Έμ§€μ—λŠ” `fetchpriority="high"`λ₯Ό μœ μ§€ν•˜λ˜ `decoding="async"`λŠ” μ‚¬μš©ν•˜μ§€ μ•Šμ•„ λΉ λ₯΄κ²Œ λ™κΈ°μ μœΌλ‘œ 그렀지도둝 ν•˜λ©°, λ‹€λ₯Έ μš”μ†Œλ“€μ—μ„œ SVGλ₯Ό μ‚¬μš©ν•  λ•ŒλŠ” 이 사싀을 μΈμ§€ν•˜κ³  λ¬΄λΆ„λ³„ν•œ 속성 μ μš©μ„ μ§€μ–‘ν•©λ‹ˆλ‹€.

## 2026-07-10 - Remove unnecessary DOMPurify for performance
**Learning:** μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ `textContent`와 같은 μ•ˆμ „ν•œ DOM API만 μ‚¬μš©ν•˜κ³  `innerHTML` λ“±μ˜ μœ„ν—˜ν•œ 싱크λ₯Ό μ‚¬μš©ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ DOMPurify와 같은 라이브러리λ₯Ό 톡해 Trusted Types 정책을 생성할 ν•„μš”κ°€ μ—†μŒ.
Expand Down
9 changes: 5 additions & 4 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,23 @@
**Vulnerability:** μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ— Trusted Typesκ°€ μ μš©λ˜μ§€ μ•Šμ•„, ν–₯ν›„ innerHTMLκ³Ό 같은 μ•ˆμ „ν•˜μ§€ μ•Šμ€ DOM sinkκ°€ λ„μž…λ  경우 잠재적인 DOM 기반 XSS 곡격에 μ·¨μ•½ν•΄μ§ˆ 수 있음.
**Learning:** μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ `textContent`와 같은 μ•ˆμ „ν•œ DOM APIλ§Œμ„ μ‚¬μš©ν•˜κ³  μœ„ν—˜ν•œ sinkκ°€ μ—†κΈ° λ•Œλ¬Έμ—, Trusted Types μ •μ±…μ΄λ‚˜ DOMPurify 같은 μ™ΈλΆ€ μƒˆλ‹ˆνƒ€μ΄μ € 없이도 CSPλ₯Ό 톡해 λ„€μ΄ν‹°λΈŒν•˜κ²Œ `require-trusted-types-for 'script'`λ₯Ό κ°•μ œν•  수 있음.
**Prevention:** 적용 κ°€λŠ₯ν•  λ•ŒλŠ” 항상 CSP에 Trusted Typesλ₯Ό μ μš©ν•˜μ—¬ DOM XSS νšŒκ·€λ₯Ό μ„ μ œμ μœΌλ‘œ λ°©μ§€ν•΄μ•Ό 함.

## 2026-07-03 - Native Trusted Types enforcement
**Vulnerability:** Trusted Types μ •μ±… λΆ€μž¬λ‘œ μΈν•œ DOM 기반 XSS (Cross-Site Scripting) 취약점 μœ„ν—˜.
**Learning:** 이 정적 μ›Ήμ‚¬μ΄νŠΈλŠ” `innerHTML` 같은 μœ„ν—˜ν•œ Sinkλ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šκ³  `textContent`, `setAttribute` λ“± μ•ˆμ „ν•œ DOM APIλ§Œμ„ μ‚¬μš©ν•˜κ³  μžˆμœΌλ―€λ‘œ, λ³„λ„μ˜ Trusted Types μ •μ±…μ΄λ‚˜ μ™ΈλΆ€ Sanitizer(예: DOMPurify) 없이도 CSPμ—μ„œ `require-trusted-types-for 'script'`λ₯Ό μ•ˆμ „ν•˜κ²Œ κΈ°λ³Έ κ°•μ œν•  수 μžˆμŒμ„ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.
**Prevention:** CSP에 `require-trusted-types-for 'script'`λ₯Ό μ μš©ν•˜μ—¬ XSSλ₯Ό λ°©μ–΄ν•˜κ³ , μ•žμœΌλ‘œλ„ μ•ˆμ „ν•œ DOM API만 μ‚¬μš©ν•˜λ„λ‘ ν•©λ‹ˆλ‹€. λΆ€λ“μ΄ν•˜κ²Œ `innerHTML`을 λ„μž…ν•΄μ•Ό ν•  κ²½μš°μ—λŠ” λ°˜λ“œμ‹œ μ μ ˆν•œ Sanitizerλ₯Ό ν•¨κ»˜ ꡬ성해야 ν•©λ‹ˆλ‹€.

## 2026-07-01 - Add Trusted Types Policy via DOMPurify
**Vulnerability:** Application lacked Trusted Types enforcement, which left it potentially vulnerable to DOM-based XSS if DOM sinks (like `innerHTML`) were manipulated.
**Learning:** Enforcing `require-trusted-types-for 'script'` in CSP causes Chromium-based browsers to throw a Trusted Types violation (a `TypeError`) when a string is assigned to a DOM sink without a registered policy, rather than crashing the browser.
**Prevention:** When a default Trusted Types policy is needed, pair the CSP `require-trusted-types-for 'script'` directive with a defensively loaded sanitizer such as DOMPurify, defer the scripts in dependency order, and keep policy creation wrapped so an existing policy or CSP restriction does not break page load.

## 2026-07-08 - Trusted Types κΈ°λ³Έ λ°©μ–΄ 적용
**Vulnerability:** DOM 기반 XSS (μ•ˆμ „ν•˜μ§€ μ•Šμ€ DOM 싱크 λ…ΈμΆœ μœ„ν—˜)
**Learning:** 이 앱은 주둜 `textContent`와 같은 μ•ˆμ „ν•œ DOM APIλ₯Ό μ‚¬μš©ν•˜κ³  `innerHTML` λ“±μ˜ μœ„ν—˜ν•œ 싱크λ₯Ό 피함. μ΄λŸ¬ν•œ ν™˜κ²½μ—μ„œλŠ” λΈŒλΌμš°μ € λ„€μ΄ν‹°λΈŒμΈ `require-trusted-types-for 'script'` CSP κ·œμΉ™μ΄ 1μ°¨ 방어선이며, κΈ°λ³Έ Trusted Types μ •μ±…κ³Ό DOMPurifyλŠ” μ‹€μ œ HTML 싱크 λ˜λŠ” ν˜Έν™˜μ„± μš”κ΅¬κ°€ μžˆμ„ λ•Œ λ°©μ–΄μ μœΌλ‘œ λ‘œλ“œν•΄μ•Ό 함.
**Learning:** 이 앱은 주둜 `textContent`와 같은 μ•ˆμ „ν•œ DOM API μ‚¬μš©μ„ ν•˜κ³  `innerHTML` λ“±μ˜ μœ„ν—˜ν•œ 싱크λ₯Ό 피함. μ΄λŸ¬ν•œ ν™˜κ²½μ—μ„œλŠ” λΈŒλΌμš°μ € λ„€μ΄ν‹°λΈŒμΈ `require-trusted-types-for 'script'` CSP κ·œμΉ™μ΄ 1μ°¨ 방어선이며, κΈ°λ³Έ Trusted Types μ •μ±…κ³Ό DOMPurifyλŠ” μ‹€μ œ HTML 싱크 λ˜λŠ” ν˜Έν™˜μ„± μš”κ΅¬κ°€ μžˆμ„ λ•Œ λ°©μ–΄μ μœΌλ‘œ λ‘œλ“œν•΄μ•Ό 함.
**Prevention:** μƒˆλ‘œμš΄ κΈ°λŠ₯을 μΆ”κ°€ν•  λ•Œ μ•±μ˜ DOM API μ‚¬μš© 방식을 λ¨Όμ € νŒŒμ•…ν•˜κ³ , λ„€μ΄ν‹°λΈŒ Trusted Types CSP만으둜 μΆ©λΆ„ν•œμ§€ λ˜λŠ” DOMPurify 기반 κΈ°λ³Έ 정책이 ν•„μš”ν•œμ§€ νŒλ‹¨ν•  것. κΈ°λ³Έ 정책을 μœ μ§€ν•œλ‹€λ©΄ `window.trustedTypes`와 `window.DOMPurify`λ₯Ό ν™•μΈν•˜κ³  `try/catch`둜 감싸 νŽ˜μ΄μ§€ λ‘œλ“œλ₯Ό κΉ¨μ§€ μ•Šλ„λ‘ ν•  것.
## 2026-07-12 - Strict CSP in Component Gallery
**Vulnerability:** Weak Content-Security Policy due to lack of headers and usage of inline scripts/styles in `components/index.html`.
**Learning:** Adding a strict CSP (`style-src 'self'`) breaks inline HTML style attributes and inline `data:` image URIs in CSS, requiring extraction into CSS classes and explicit scheme additions (e.g., `img-src 'self' data:;`).
**Prevention:** Always refactor inline `<script>` and `<style>` blocks to external files, and replace inline `style="..."` attributes with utility classes before rolling out a strict CSP.
## 2024-05-24 - Add Input Validation for Language Selection
**Vulnerability:** Missing input validation on `setLanguage()` could allow invalid strings (like Prototype Pollution payloads or arbitrary text) to be applied to the DOM (`lang` attribute) and stored in `localStorage`.
**Learning:** The global `setLanguage` function assumed inputs would only come from predefined button clicks, skipping runtime validation.
**Prevention:** Always sanitize and validate function arguments at the application boundary, even if the primary caller is trusted, to enforce defense in depth.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
- **μ„±λŠ₯ μ΅œμ ν™”**: μ• ν”Œλ¦¬μΌ€μ΄μ…˜ ν™˜κ²½μ΄ μ•ˆμ „ν•œ `textContent` DOM APIλ§Œμ„ μ‚¬μš©ν•˜μ—¬ λ Œλ”λ§λ˜λ―€λ‘œ, λΆˆν•„μš”ν•œ HTML Sanitize 라이브러리(`DOMPurify`) λ‘œλ”© μŠ€ν¬λ¦½νŠΈμ™€ `security.js`λ₯Ό μ œκ±°ν•˜μ—¬ λΉŒλ“œ/배포 크기λ₯Ό 쀄이고 초기 λ‘œλ”© μ„±λŠ₯을 κ°œμ„ ν–ˆμŠ΅λ‹ˆλ‹€.
- **λ³΄μ•ˆ κ°•ν™”**: DOM XSS 곡격을 λ°©μ§€ν•˜κΈ° μœ„ν•΄ CSP에 `require-trusted-types-for 'script'`λ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ `textContent`와 같은 μ•ˆμ „ν•œ DOM API만 μ‚¬μš©ν•˜λ―€λ‘œ μ™ΈλΆ€ 라이브러리 없이 λ„€μ΄ν‹°λΈŒ λ³΄ν˜Έκ°€ κ°€λŠ₯ν•©λ‹ˆλ‹€.
- **UX κ°œμ„ **: λ‚΄λΉ„κ²Œμ΄μ…˜ 링크(`.site-nav a`, `.intro-lnb a`)에 ν˜Έλ²„ μ‹œ λΆ€λ“œλŸ¬μš΄ 색상 μ „ν™˜ νŠΈλžœμ§€μ…˜μ„ μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.
- **λ³΄μ•ˆ κ°œμ„ **: `i18n.js`의 `setLanguage()` ν•¨μˆ˜μ— ν—ˆμš©λœ 언어인지 ν™•μΈν•˜λŠ” μž…λ ₯κ°’ 검증(Input Validation) λ‘œμ§μ„ μΆ”κ°€ν•˜μ—¬ Prototype Pollution 및 μœ νš¨ν•˜μ§€ μ•Šμ€ μƒνƒœ μ£Όμž…μ„ λ°©μ§€ν–ˆμŠ΅λ‹ˆλ‹€.
- **μ„±λŠ₯ κ°œμ„ **: `i18n.js`μ—μ„œ 초기 λ‘œλ“œ μ‹œ κΈ°λ³Έ μ–Έμ–΄κ°€ ν•œκ΅­μ–΄(ko)인 경우 λΆˆν•„μš”ν•œ DOM 순회 및 ν…μŠ€νŠΈ μ—…λ°μ΄νŠΈλ₯Ό μƒλž΅ν•˜λ„λ‘ κ°œμ„ ν–ˆμŠ΅λ‹ˆλ‹€.
- **ν…ŒμŠ€νŠΈ μΆ”κ°€**: λ‹€κ΅­μ–΄ 처리 둜직의 무결성을 κ²€μ¦ν•˜κΈ° μœ„ν•΄ `test_i18n.html` ν…ŒμŠ€νŠΈ νŒŒμΌμ„ μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.
6 changes: 0 additions & 6 deletions commit_message.txt

This file was deleted.

7 changes: 7 additions & 0 deletions i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,13 @@ let footerLogo = null;
let currentLang = null;

function setLanguage(lang) {
// πŸ›‘οΈ Sentinel: Validate input to prevent prototype pollution or invalid state injection
const allowedLanguages = ["ko", "en"];
if (!allowedLanguages.includes(lang)) {
console.warn(`[Security] Invalid language requested: ${lang}. Falling back to default.`);
lang = "ko";
}

if (currentLang === lang) return; // Skip if already in the requested language

const dict = messages[lang] || messages.ko;
Expand Down
9 changes: 4 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ <h1 data-i18n="hero.title">λ§₯λ½μ§€ν˜œ 연ꡬ싀</h1>
</div>

<div class="hero-visual">
<!-- ⚑ Bolt: Removed decoding="async" for LCP image to paint synchronously and as fast as possible -->
<img class="context-art" src="assets/context-thread-map.svg" alt="" aria-hidden="true" width="760" height="560" fetchpriority="high">
<img class="context-art" src="assets/context-thread-map.svg" alt="" aria-hidden="true" width="760" height="560" fetchpriority="high" decoding="async">
<div class="ladder" role="list" aria-label="Data to wisdom ladder">
<div class="ladder-row" role="listitem">
<span>Data</span>
Expand Down Expand Up @@ -194,7 +193,7 @@ <h2 data-i18n="dikw.title">DIKWλŠ” νŒλ‹¨ 흐름을 μ κ²€ν•˜λŠ” μ§ˆλ¬Έμž…λ‹ˆλ‹€
PPTX의 흐름은 κΈ°μ—… 자료, λ§₯락화, νŒλ‹¨ 포인트, μ‹€ν–‰ μ—°κ²°μž…λ‹ˆλ‹€. DIKWλŠ” 이 흐름을 μžλ™ μƒμŠΉ μœ„κ³„λ‘œ λ³΄μžλŠ” 말이 μ•„λ‹ˆλΌ, μžλ£Œκ°€ νŒλ‹¨ 근거둜 쓰일 μ€€λΉ„κ°€ λ˜μ—ˆλŠ”μ§€ λ¬»λŠ” 점검 μ§ˆλ¬Έμž…λ‹ˆλ‹€.
</p>
</div>
<!-- ⚑ Bolt: Lazy load off-screen image. Decoding="async" is kept as a minor optimization, though its effect on SVGs is negligible. -->
<!-- ⚑ Bolt: Lazy load off-screen image -->
<img class="section-diagram" src="assets/dikw-checkpoints.svg" alt="" aria-hidden="true" loading="lazy" width="1120" height="330" decoding="async">
<div class="dikw-grid">
<article>
Expand Down Expand Up @@ -272,7 +271,7 @@ <h2 data-i18n="references.title">μ°Έκ³ λ¬Έν—Œ</h2>

<section id="logo" class="section logo-story">
<div class="logo-mark">
<!-- ⚑ Bolt: Lazy load off-screen image. Decoding="async" is kept as a minor optimization. -->
<!-- ⚑ Bolt: Lazy load off-screen image -->
<img src="assets/context-wisdom-lab-avatar.svg" alt="Contextual Wisdom Lab square mark" loading="lazy" width="500" height="500" decoding="async">
</div>
<div>
Expand Down Expand Up @@ -418,7 +417,7 @@ <h2 data-i18n="work.title">μ—°κ΅¬μ—μ„œ μ œν’ˆμœΌλ‘œ</h2>
</main>

<footer class="site-footer">
<!-- ⚑ Bolt: Lazy load off-screen image. Decoding="async" is kept as a minor optimization. -->
<!-- ⚑ Bolt: Lazy load off-screen image -->
<img
id="footer-logo"
src="assets/context-wisdom-lab-logo.svg"
Expand Down
6 changes: 6 additions & 0 deletions test_i18n.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
assertEqual(currentLang, "ko", "Language should be 'ko' after setting back");
assertEqual(titleNode.textContent, "λ§₯λ½μ§€ν˜œ 연ꡬ싀", "Title text back to Korean");

// Test 4: Sentinel Security - Input Validation for untrusted inputs
setLanguage("__proto__");
assertEqual(currentLang, "ko", "Language should safely fallback to 'ko' on invalid input");
setLanguage("<script>alert(1)<\/script>");
assertEqual(currentLang, "ko", "Language should safely fallback to 'ko' on XSS payloads");

console.log(`Test Summary: ${testsPassed}/${testsTotal} passed.`);

if (testsPassed === testsTotal) {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
18 changes: 18 additions & 0 deletions tests/test_i18n_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Test i18n security input validation."""

def test_i18n_input_validation() -> None:
"""Test that allowedLanguages validation logic is correctly implemented in i18n.js."""
with open("i18n.js", "r", encoding="utf-8") as f:
content = f.read()

# Check for whitelist validation
assert "allowedLanguages = [\"ko\", \"en\"]" in content or "allowedLanguages = ['ko', 'en']" in content
assert "allowedLanguages.includes" in content

def test_i18n_html_security_tests_present() -> None:
"""Test that explicit __proto__ and XSS payload checks exist in the HTML test harness."""
with open("test_i18n.html", "r", encoding="utf-8") as f:
content = f.read()

assert "setLanguage(\"__proto__\")" in content
assert "setLanguage(\"<script>alert(1)<\\/script>\")" in content
25 changes: 6 additions & 19 deletions tests/test_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None
self.images.append(dict(attrs))


def _images() -> list[dict[str, str | None]]:
parser = _ImageParser()
parser.feed(INDEX.read_text(encoding="utf-8"))
return parser.images


def _rule(selector: str) -> str:
css = STYLES.read_text(encoding="utf-8")
match = re.search(rf"{re.escape(selector)}\s*\{{(?P<body>[^}}]+)\}}", css)
Expand All @@ -49,17 +43,10 @@ def test_tall_sections_reserve_larger_intrinsic_block_size():
assert "contain-intrinsic-size: auto 1000px;" in rule


def test_lcp_image_prioritizes_synchronous_paint():
"""The hero LCP SVG keeps high priority without async decoding."""
context_art = next(img for img in _images() if img.get("class") == "context-art")

assert context_art["fetchpriority"] == "high"
assert "decoding" not in context_art


def test_non_lcp_images_decode_asynchronously():
"""Non-LCP images use async decoding to reduce main-thread jank."""
non_lcp_images = [img for img in _images() if img.get("class") != "context-art"]
def test_images_decode_without_blocking_rendering():
"""All site images opt into asynchronous decoding."""
parser = _ImageParser()
parser.feed(INDEX.read_text(encoding="utf-8"))

assert non_lcp_images
assert all(img.get("decoding") == "async" for img in non_lcp_images)
assert parser.images
assert all(image.get("decoding") == "async" for image in parser.images)
Loading