diff --git a/REUSE.toml b/REUSE.toml index f3ccbd90d..40648d946 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -16,3 +16,11 @@ SPDX-FileCopyrightText = "asciinema contributors" SPDX-License-Identifier = "Apache-2.0" # NOTE: This file must be placed before deploying tutorials that use ::cast directives. # Download from https://github.com/asciinema/asciinema-player/releases + +[[annotations]] +path = "hugo/static/js/vendor/asciinema-player.css" +SPDX-FileCopyrightText = "asciinema contributors" +SPDX-License-Identifier = "Apache-2.0" +# NOTE: This file must be placed before deploying tutorials that use ::cast directives. +# Download from https://github.com/asciinema/asciinema-player/releases + diff --git a/hugo/assets/js/__tests__/cast.test.ts b/hugo/assets/js/__tests__/cast.test.ts new file mode 100644 index 000000000..ddf52fd9d --- /dev/null +++ b/hugo/assets/js/__tests__/cast.test.ts @@ -0,0 +1,66 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +// Provide a minimal AsciinemaPlayer stub on window. +const mockCreate = vi.fn(); +beforeEach(() => { + vi.resetModules(); + document.body.innerHTML = ''; + (window as unknown as Record)['AsciinemaPlayer'] = { create: mockCreate }; + mockCreate.mockClear(); +}); + +it('mounts player on .tut-cast elements in viewport', async () => { + document.body.innerHTML = ` +
`; + const { initCast } = await import('../cast.js'); + await initCast({ skipObserver: true }); // bypass IO for unit test + expect(mockCreate).toHaveBeenCalledOnce(); + const [src, el, opts] = mockCreate.mock.calls[0]; + expect(src).toBe('https://example.com/demo.cast'); + expect(el).toBeInstanceOf(HTMLElement); + expect(opts.rows).toBe(24); + expect(opts.speed).toBe(1.5); +}); + +it('does not double-mount an already mounted element', async () => { + document.body.innerHTML = ` +
`; + const { initCast } = await import('../cast.js'); + await initCast({ skipObserver: true }); + expect(mockCreate).not.toHaveBeenCalled(); +}); + +it('falls back to immediate mount when IntersectionObserver absent', async () => { + document.body.innerHTML = ` +
`; + // Remove IO from window to simulate absent support. + const origIO = (window as unknown as Record)['IntersectionObserver']; + delete (window as unknown as Record)['IntersectionObserver']; + const { initCast } = await import('../cast.js'); + await initCast({}); + expect(mockCreate).toHaveBeenCalledOnce(); + (window as unknown as Record)['IntersectionObserver'] = origIO; +}); + +it('passes idle-limit as idleTimeLimit to player', async () => { + document.body.innerHTML = ` +
`; + const { initCast } = await import('../cast.js'); + await initCast({ skipObserver: true }); + const [, , opts] = mockCreate.mock.calls[0]; + expect(opts.idleTimeLimit).toBe(2); +}); + +it('removes the placeholder pre after mounting', async () => { + document.body.innerHTML = ` +
+
Loading…
+
`; + const { initCast } = await import('../cast.js'); + await initCast({ skipObserver: true }); + expect(document.querySelector('.tut-cast__placeholder')).toBeNull(); +}); diff --git a/hugo/assets/js/cast.ts b/hugo/assets/js/cast.ts index cb0ff5c3b..941f6a12a 100644 --- a/hugo/assets/js/cast.ts +++ b/hugo/assets/js/cast.ts @@ -1 +1,117 @@ -export {}; +// cast.ts — lazy-mount asciinema player on .tut-cast[data-tut-cast] elements. +// The asciinema-player is vendored as an IIFE (sets window.AsciinemaPlayer), +// so we inject a