mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 05:37:21 +00:00
refactor(html): SkinMixin -> SkinElement (#1159)
Co-authored-by: Rahim <rahim.alwer@gmail.com>
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render/minimal';
|
||||
import { playbackRate } from '@videojs/skins/default/tailwind/audio.tailwind';
|
||||
import {
|
||||
@@ -17,9 +16,10 @@ import {
|
||||
time,
|
||||
tooltipState,
|
||||
} from '@videojs/skins/minimal/tailwind/audio.tailwind';
|
||||
import { createTemplate } from '@videojs/utils/dom';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { safeDefine } from '../safe-define';
|
||||
import { SkinMixin } from '../skin-mixin';
|
||||
import { SkinElement } from '../skin-element';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
@@ -137,9 +137,9 @@ function getTemplateHTML() {
|
||||
`;
|
||||
}
|
||||
|
||||
export class MinimalAudioSkinTailwindElement extends SkinMixin(ReactiveElement) {
|
||||
export class MinimalAudioSkinTailwindElement extends SkinElement {
|
||||
static readonly tagName = 'audio-minimal-skin-tailwind';
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
static template = createTemplate(getTemplateHTML());
|
||||
}
|
||||
|
||||
safeDefine(MinimalAudioSkinTailwindElement);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render/minimal';
|
||||
import { createShadowStyle, createTemplate } from '@videojs/utils/dom';
|
||||
import { safeDefine } from '../safe-define';
|
||||
import { createStyles, SkinMixin } from '../skin-mixin';
|
||||
import { SkinElement } from '../skin-element';
|
||||
import styles from './minimal-skin.css?inline';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
@@ -118,10 +118,10 @@ function getTemplateHTML() {
|
||||
`;
|
||||
}
|
||||
|
||||
export class MinimalAudioSkinElement extends SkinMixin(ReactiveElement) {
|
||||
export class MinimalAudioSkinElement extends SkinElement {
|
||||
static readonly tagName = 'audio-minimal-skin';
|
||||
static styles = createStyles(styles);
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
static styles = createShadowStyle(styles);
|
||||
static template = createTemplate(getTemplateHTML());
|
||||
}
|
||||
|
||||
safeDefine(MinimalAudioSkinElement);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render';
|
||||
import {
|
||||
button,
|
||||
@@ -17,9 +16,10 @@ import {
|
||||
time,
|
||||
tooltipState,
|
||||
} from '@videojs/skins/default/tailwind/audio.tailwind';
|
||||
import { createTemplate } from '@videojs/utils/dom';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { safeDefine } from '../safe-define';
|
||||
import { SkinMixin } from '../skin-mixin';
|
||||
import { SkinElement } from '../skin-element';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
@@ -132,9 +132,9 @@ function getTemplateHTML() {
|
||||
`;
|
||||
}
|
||||
|
||||
export class AudioSkinTailwindElement extends SkinMixin(ReactiveElement) {
|
||||
export class AudioSkinTailwindElement extends SkinElement {
|
||||
static readonly tagName = 'audio-skin-tailwind';
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
static template = createTemplate(getTemplateHTML());
|
||||
}
|
||||
|
||||
safeDefine(AudioSkinTailwindElement);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render';
|
||||
import { createShadowStyle, createTemplate } from '@videojs/utils/dom';
|
||||
import { safeDefine } from '../safe-define';
|
||||
import { createStyles, SkinMixin } from '../skin-mixin';
|
||||
import { SkinElement } from '../skin-element';
|
||||
import styles from './skin.css?inline';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
@@ -113,10 +113,10 @@ function getTemplateHTML() {
|
||||
`;
|
||||
}
|
||||
|
||||
export class AudioSkinElement extends SkinMixin(ReactiveElement) {
|
||||
export class AudioSkinElement extends SkinElement {
|
||||
static readonly tagName = 'audio-skin';
|
||||
static styles = createStyles(styles);
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
static styles = createShadowStyle(styles);
|
||||
static template = createTemplate(getTemplateHTML());
|
||||
}
|
||||
|
||||
safeDefine(AudioSkinElement);
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { namedNodeMapToObject } from '@videojs/utils/dom';
|
||||
import { ensureGlobalStyle, namedNodeMapToObject } from '@videojs/utils/dom';
|
||||
import { safeDefine } from '../safe-define';
|
||||
import styles from './skin.css?inline';
|
||||
|
||||
const STYLES_ID = '__media-background-styles';
|
||||
|
||||
function ensureBackgroundStyles(): void {
|
||||
if (document.getElementById(STYLES_ID)) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = STYLES_ID;
|
||||
style.textContent = styles;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function getTemplateHTML(_attrs: Record<string, string>) {
|
||||
return /*html*/ `
|
||||
<media-container>
|
||||
@@ -31,7 +23,7 @@ export class BackgroundVideoSkinElement extends ReactiveElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
ensureBackgroundStyles();
|
||||
ensureGlobalStyle(STYLES_ID, styles);
|
||||
|
||||
if (!this.shadowRoot) {
|
||||
this.attachShadow((this.constructor as typeof BackgroundVideoSkinElement).shadowRootOptions);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import {
|
||||
applyShadowStyles,
|
||||
createShadowStyle,
|
||||
ensureGlobalStyle,
|
||||
renderTemplate,
|
||||
type ShadowStyle,
|
||||
} from '@videojs/utils/dom';
|
||||
import rootStyles from './base.css?inline';
|
||||
import sharedStyles from './shared.css?inline';
|
||||
|
||||
const STYLES_ID = '__media-styles';
|
||||
const sharedSheet = createShadowStyle(sharedStyles);
|
||||
|
||||
/**
|
||||
* Base element for skin definitions. Attaches a shadow root, clones
|
||||
* `static template` into it, and applies shared + per-skin styles
|
||||
* via `adoptedStyleSheets` (or `<style>` fallback).
|
||||
*/
|
||||
export class SkinElement extends ReactiveElement {
|
||||
static shadowRootOptions: ShadowRootInit = { mode: 'open' };
|
||||
static styles?: ShadowStyle;
|
||||
static template?: HTMLTemplateElement | null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
ensureGlobalStyle(STYLES_ID, rootStyles);
|
||||
|
||||
if (!this.shadowRoot) {
|
||||
const ctor = this.constructor as typeof SkinElement;
|
||||
this.attachShadow(ctor.shadowRootOptions);
|
||||
|
||||
if (ctor.template) {
|
||||
renderTemplate(this.shadowRoot!, ctor.template);
|
||||
}
|
||||
|
||||
const sheets: ShadowStyle[] = [sharedSheet];
|
||||
if (ctor.styles) {
|
||||
sheets.push(ctor.styles);
|
||||
}
|
||||
applyShadowStyles(this.shadowRoot!, sheets);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import type { ReactiveElement } from '@videojs/element';
|
||||
import type { Constructor } from '@videojs/utils/types';
|
||||
import rootStyles from './base.css?inline';
|
||||
import sharedStyles from './shared.css?inline';
|
||||
|
||||
const STYLES_ID = '__media-styles';
|
||||
type SkinStyles = CSSStyleSheet | string;
|
||||
|
||||
function ensureRootStyles(): void {
|
||||
const doc = globalThis.document;
|
||||
if (!doc || doc.getElementById(STYLES_ID)) return;
|
||||
|
||||
const style = doc.createElement('style');
|
||||
style.id = STYLES_ID;
|
||||
style.textContent = rootStyles;
|
||||
doc.head.appendChild(style);
|
||||
}
|
||||
|
||||
function isConstructableStyleSheet(value: SkinStyles): value is CSSStyleSheet {
|
||||
return typeof globalThis.CSSStyleSheet !== 'undefined' && value instanceof globalThis.CSSStyleSheet;
|
||||
}
|
||||
|
||||
function getStyleText(style: SkinStyles): string {
|
||||
if (typeof style === 'string') return style;
|
||||
|
||||
return Array.from(style.cssRules)
|
||||
.map((rule) => rule.cssText)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function applyShadowStyles(shadowRoot: ShadowRoot, styles: SkinStyles[]): void {
|
||||
if (styles.every(isConstructableStyleSheet) && 'adoptedStyleSheets' in shadowRoot) {
|
||||
shadowRoot.adoptedStyleSheets = styles;
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = shadowRoot.ownerDocument;
|
||||
for (const styleText of styles.map(getStyleText)) {
|
||||
const style = doc.createElement('style');
|
||||
style.textContent = styleText;
|
||||
shadowRoot.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
const sharedSheet = createStyles(sharedStyles);
|
||||
|
||||
/**
|
||||
* Mixin for skin elements that renders the template from a static
|
||||
* `getTemplateHTML` method into a shadow root. Native `<slot>` elements
|
||||
* handle light DOM projection automatically.
|
||||
*
|
||||
* When `static styles` is set, the stylesheet is adopted into the
|
||||
* shadow root via `adoptedStyleSheets`.
|
||||
*/
|
||||
export function SkinMixin<Base extends Constructor<ReactiveElement>>(
|
||||
BaseClass: Base
|
||||
): Base & { shadowRootOptions: ShadowRootInit; styles?: SkinStyles } {
|
||||
class SkinElement extends (BaseClass as Constructor<ReactiveElement>) {
|
||||
static shadowRootOptions: ShadowRootInit = { mode: 'open' };
|
||||
static styles?: SkinStyles;
|
||||
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
|
||||
ensureRootStyles();
|
||||
|
||||
if (!this.shadowRoot) {
|
||||
const ctor = this.constructor as typeof SkinElement & { getTemplateHTML?: () => string };
|
||||
this.attachShadow(ctor.shadowRootOptions);
|
||||
|
||||
if (ctor.getTemplateHTML) {
|
||||
this.shadowRoot!.innerHTML = ctor.getTemplateHTML();
|
||||
}
|
||||
|
||||
const sheets: SkinStyles[] = [sharedSheet];
|
||||
if (ctor.styles) {
|
||||
sheets.push(ctor.styles);
|
||||
}
|
||||
applyShadowStyles(this.shadowRoot!, sheets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SkinElement as unknown as Base & { shadowRootOptions: ShadowRootInit; styles?: SkinStyles };
|
||||
}
|
||||
|
||||
/** Create a constructable stylesheet when available, otherwise return raw CSS. */
|
||||
export function createStyles(css: string): SkinStyles {
|
||||
if (typeof globalThis.CSSStyleSheet === 'undefined') {
|
||||
return css;
|
||||
}
|
||||
|
||||
const sheet = new globalThis.CSSStyleSheet();
|
||||
sheet.replaceSync(css);
|
||||
return sheet;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render/minimal';
|
||||
import {
|
||||
bufferingIndicator,
|
||||
@@ -22,9 +21,10 @@ import {
|
||||
time,
|
||||
tooltipState,
|
||||
} from '@videojs/skins/minimal/tailwind/video.tailwind';
|
||||
import { createTemplate } from '@videojs/utils/dom';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { safeDefine } from '../safe-define';
|
||||
import { SkinMixin } from '../skin-mixin';
|
||||
import { SkinElement } from '../skin-element';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
@@ -199,9 +199,9 @@ function getTemplateHTML() {
|
||||
`;
|
||||
}
|
||||
|
||||
export class MinimalVideoSkinTailwindElement extends SkinMixin(ReactiveElement) {
|
||||
export class MinimalVideoSkinTailwindElement extends SkinElement {
|
||||
static readonly tagName = 'video-minimal-skin-tailwind';
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
static template = createTemplate(getTemplateHTML());
|
||||
}
|
||||
|
||||
safeDefine(MinimalVideoSkinTailwindElement);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render/minimal';
|
||||
import { createShadowStyle, createTemplate } from '@videojs/utils/dom';
|
||||
import { safeDefine } from '../safe-define';
|
||||
import { createStyles, SkinMixin } from '../skin-mixin';
|
||||
import { SkinElement } from '../skin-element';
|
||||
import styles from './minimal-skin.css?inline';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
@@ -166,10 +166,10 @@ function getTemplateHTML() {
|
||||
`;
|
||||
}
|
||||
|
||||
export class MinimalVideoSkinElement extends SkinMixin(ReactiveElement) {
|
||||
export class MinimalVideoSkinElement extends SkinElement {
|
||||
static readonly tagName = 'video-minimal-skin';
|
||||
static styles = createStyles(styles);
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
static styles = createShadowStyle(styles);
|
||||
static template = createTemplate(getTemplateHTML());
|
||||
}
|
||||
|
||||
safeDefine(MinimalVideoSkinElement);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render';
|
||||
import {
|
||||
bufferingIndicator,
|
||||
@@ -22,9 +21,10 @@ import {
|
||||
time,
|
||||
tooltipState,
|
||||
} from '@videojs/skins/default/tailwind/video.tailwind';
|
||||
import { createTemplate } from '@videojs/utils/dom';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { safeDefine } from '../safe-define';
|
||||
import { SkinMixin } from '../skin-mixin';
|
||||
import { SkinElement } from '../skin-element';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
@@ -194,9 +194,9 @@ function getTemplateHTML() {
|
||||
`;
|
||||
}
|
||||
|
||||
export class VideoSkinTailwindElement extends SkinMixin(ReactiveElement) {
|
||||
export class VideoSkinTailwindElement extends SkinElement {
|
||||
static readonly tagName = 'video-skin-tailwind';
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
static template = createTemplate(getTemplateHTML());
|
||||
}
|
||||
|
||||
safeDefine(VideoSkinTailwindElement);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render';
|
||||
import { createShadowStyle, createTemplate } from '@videojs/utils/dom';
|
||||
import { safeDefine } from '../safe-define';
|
||||
import { createStyles, SkinMixin } from '../skin-mixin';
|
||||
import { SkinElement } from '../skin-element';
|
||||
import styles from './skin.css?inline';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
@@ -164,10 +164,10 @@ function getTemplateHTML() {
|
||||
`;
|
||||
}
|
||||
|
||||
export class VideoSkinElement extends SkinMixin(ReactiveElement) {
|
||||
export class VideoSkinElement extends SkinElement {
|
||||
static readonly tagName = 'video-skin';
|
||||
static styles = createStyles(styles);
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
static styles = createShadowStyle(styles);
|
||||
static template = createTemplate(getTemplateHTML());
|
||||
}
|
||||
|
||||
safeDefine(VideoSkinElement);
|
||||
|
||||
@@ -7,9 +7,16 @@ export { listen } from './listen';
|
||||
export { tryHidePopover, tryShowPopover } from './popover';
|
||||
export { isHTMLAudioElement, isHTMLMediaElement, isHTMLVideoElement } from './predicates';
|
||||
export { type RafThrottled, rafThrottle } from './raf-throttle';
|
||||
export {
|
||||
applyShadowStyles,
|
||||
createShadowStyle,
|
||||
ensureGlobalStyle,
|
||||
type ShadowStyle,
|
||||
} from './shadow-styles';
|
||||
export { getSlottedElement, querySlot } from './slotted';
|
||||
export { applyStyles, resolveCSSLength } from './style';
|
||||
export { supportsAnchorPositioning, supportsAnimationFrame, supportsIdleCallback } from './supports';
|
||||
export { createTemplate, renderTemplate } from './template';
|
||||
export { findTrackElement, getTextTrackList } from './text-track';
|
||||
export { serializeTimeRanges } from './time-ranges';
|
||||
export type { CustomElement, CustomElementCallbacks } from './types';
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
export type ShadowStyle = CSSStyleSheet | string;
|
||||
|
||||
/** Inject a `<style>` tag into `document.head` once (idempotent by `id`). */
|
||||
export function ensureGlobalStyle(id: string, css: string): void {
|
||||
const doc = globalThis.document;
|
||||
if (!doc || doc.getElementById(id)) return;
|
||||
|
||||
const style = doc.createElement('style');
|
||||
style.id = id;
|
||||
style.textContent = css;
|
||||
doc.head.appendChild(style);
|
||||
}
|
||||
|
||||
function isConstructableStyleSheet(value: ShadowStyle): value is CSSStyleSheet {
|
||||
return typeof globalThis.CSSStyleSheet !== 'undefined' && value instanceof globalThis.CSSStyleSheet;
|
||||
}
|
||||
|
||||
function getStyleText(style: ShadowStyle): string {
|
||||
if (typeof style === 'string') return style;
|
||||
|
||||
return Array.from(style.cssRules)
|
||||
.map((rule) => rule.cssText)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/** Create a constructable stylesheet when available, otherwise return raw CSS. */
|
||||
export function createShadowStyle(css: string): ShadowStyle {
|
||||
if (typeof globalThis.CSSStyleSheet === 'undefined') {
|
||||
return css;
|
||||
}
|
||||
|
||||
const sheet = new globalThis.CSSStyleSheet();
|
||||
sheet.replaceSync(css);
|
||||
return sheet;
|
||||
}
|
||||
|
||||
/** Apply styles to a shadow root using `adoptedStyleSheets` when available, falling back to `<style>` injection. */
|
||||
export function applyShadowStyles(shadowRoot: ShadowRoot, styles: ShadowStyle[]): void {
|
||||
if (styles.every(isConstructableStyleSheet) && 'adoptedStyleSheets' in shadowRoot) {
|
||||
shadowRoot.adoptedStyleSheets = styles;
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = shadowRoot.ownerDocument;
|
||||
for (const styleText of styles.map(getStyleText)) {
|
||||
const style = doc.createElement('style');
|
||||
style.textContent = styleText;
|
||||
shadowRoot.appendChild(style);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/** Create an `HTMLTemplateElement` from an HTML string, or `null` when `document` is unavailable (SSR). */
|
||||
export function createTemplate(html: string): HTMLTemplateElement | null {
|
||||
const doc = globalThis.document;
|
||||
if (!doc) return null;
|
||||
|
||||
const template = doc.createElement('template');
|
||||
template.innerHTML = html;
|
||||
return template;
|
||||
}
|
||||
|
||||
/** Deep-clone a template's content into a container. */
|
||||
export function renderTemplate(container: Element | ShadowRoot, template: HTMLTemplateElement): void {
|
||||
container.appendChild(container.ownerDocument.importNode(template.content, true));
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { applyShadowStyles, createShadowStyle, ensureGlobalStyle } from '../shadow-styles';
|
||||
|
||||
describe('createShadowStyle', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('returns a CSSStyleSheet when constructable stylesheets are available', () => {
|
||||
const result = createShadowStyle('div { color: red; }');
|
||||
expect(result).toBeInstanceOf(CSSStyleSheet);
|
||||
});
|
||||
|
||||
it('returns raw CSS string when CSSStyleSheet is unavailable', () => {
|
||||
vi.stubGlobal('CSSStyleSheet', undefined);
|
||||
|
||||
const css = 'div { color: red; }';
|
||||
const result = createShadowStyle(css);
|
||||
expect(result).toBe(css);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyShadowStyles', () => {
|
||||
function createHost(): HTMLElement {
|
||||
const host = document.createElement('div');
|
||||
host.attachShadow({ mode: 'open' });
|
||||
document.body.appendChild(host);
|
||||
return host;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('uses adoptedStyleSheets when supported and all styles are constructable', () => {
|
||||
const host = createHost();
|
||||
const shadowRoot = host.shadowRoot!;
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync('div { color: red; }');
|
||||
|
||||
let assigned: CSSStyleSheet[] = [];
|
||||
Object.defineProperty(shadowRoot, 'adoptedStyleSheets', {
|
||||
get: () => assigned,
|
||||
set: (value: CSSStyleSheet[]) => {
|
||||
assigned = value;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
applyShadowStyles(shadowRoot, [sheet]);
|
||||
expect(assigned).toContain(sheet);
|
||||
expect(shadowRoot.querySelectorAll('style').length).toBe(0);
|
||||
});
|
||||
|
||||
it('falls back to <style> injection for string styles', () => {
|
||||
const host = createHost();
|
||||
const css = 'div { color: blue; }';
|
||||
|
||||
applyShadowStyles(host.shadowRoot!, [css]);
|
||||
const styleEls = host.shadowRoot!.querySelectorAll('style');
|
||||
expect(styleEls.length).toBe(1);
|
||||
expect(styleEls[0]!.textContent).toBe(css);
|
||||
});
|
||||
|
||||
it('falls back to <style> injection when styles are mixed', () => {
|
||||
const host = createHost();
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync('div { color: red; }');
|
||||
const css = 'div { color: blue; }';
|
||||
|
||||
applyShadowStyles(host.shadowRoot!, [sheet, css]);
|
||||
const styleEls = host.shadowRoot!.querySelectorAll('style');
|
||||
expect(styleEls.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureGlobalStyle', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
document.head.innerHTML = '';
|
||||
});
|
||||
|
||||
it('injects a <style> tag into document.head', () => {
|
||||
ensureGlobalStyle('test-style', 'body { margin: 0; }');
|
||||
|
||||
const el = document.getElementById('test-style');
|
||||
expect(el).toBeInstanceOf(HTMLStyleElement);
|
||||
expect(el!.textContent).toBe('body { margin: 0; }');
|
||||
});
|
||||
|
||||
it('does not duplicate when called twice with the same id', () => {
|
||||
ensureGlobalStyle('test-dedup', 'a { color: red; }');
|
||||
ensureGlobalStyle('test-dedup', 'a { color: blue; }');
|
||||
|
||||
expect(document.querySelectorAll('#test-dedup').length).toBe(1);
|
||||
expect(document.getElementById('test-dedup')!.textContent).toBe('a { color: red; }');
|
||||
});
|
||||
|
||||
it('does nothing when document is unavailable', () => {
|
||||
vi.stubGlobal('document', undefined);
|
||||
expect(() => ensureGlobalStyle('ssr', 'body {}')).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createTemplate, renderTemplate } from '../template';
|
||||
|
||||
describe('createTemplate', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('returns an HTMLTemplateElement with parsed content', () => {
|
||||
const template = createTemplate('<div class="root"><span>Hello</span></div>');
|
||||
|
||||
expect(template).toBeInstanceOf(HTMLTemplateElement);
|
||||
expect(template!.content.querySelector('.root')).toBeTruthy();
|
||||
expect(template!.content.querySelector('span')!.textContent).toBe('Hello');
|
||||
});
|
||||
|
||||
it('returns null when document is unavailable', () => {
|
||||
vi.stubGlobal('document', undefined);
|
||||
expect(createTemplate('<div></div>')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderTemplate', () => {
|
||||
it('deep-clones template content into a container', () => {
|
||||
const template = createTemplate('<p>Hello</p><p>World</p>')!;
|
||||
const container = document.createElement('div');
|
||||
|
||||
renderTemplate(container, template);
|
||||
|
||||
expect(container.children).toHaveLength(2);
|
||||
expect(container.innerHTML).toBe('<p>Hello</p><p>World</p>');
|
||||
});
|
||||
|
||||
it('appends without clearing existing content', () => {
|
||||
const template = createTemplate('<span>new</span>')!;
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<span>existing</span>';
|
||||
|
||||
renderTemplate(container, template);
|
||||
|
||||
expect(container.children).toHaveLength(2);
|
||||
expect(container.children[0]!.textContent).toBe('existing');
|
||||
expect(container.children[1]!.textContent).toBe('new');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user