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
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,24 @@ is shown, as the H5P CMS plugins check an upload: text keeps only the tags its f
semantics do not name is dropped. Only a package that cannot play without its own copy of a library waits for a
click that names its host.

A package exported without its libraries, as H5P.com and h5p.org export them, gets them from
`@missing-elements/h5p-libraries`: one `.h5p` with the H5P hub's libraries for every content type it serves, about
14 MB, which Vite emits as a hashed asset and the page uses by default (`defaultLibraries: 'pack'`). Nothing is
behind it: since player 0.6 there is no H5P hub fallback, so a content type the pack lacks is reported as missing
libraries, and the page makes no request to a third party by itself. Since player 0.6.1 the frame upgrades content
written for an older minor version of a library to the one the pack serves, with that library's `upgrades.js`.
A package exported without its libraries, as H5P.com and h5p.org export them, gets them from the pack of
`@missing-elements/h5p-libraries`: the H5P hub's libraries for every content type it serves, one part per library,
which Vite emits as hashed assets and the page uses by default (`pack`, `defaultLibraries: 'pack'`). A package
downloads only the parts it needs, each checked against the pack's SHA-256 and kept in the browser for every package
that uses it; the build page reads the same parts. Nothing is behind the pack: since player 0.6 there is no H5P hub
fallback, so a content type the pack lacks is reported as missing libraries, and the page makes no request to a third
party by itself. The frame upgrades content written for an older minor version of a library to the one the pack
serves, with that library's `upgrades.js`, so a package that carries older libraries plays the pack's newer ones
(player 0.10; `&refresh-libraries=off` plays them as exported), and the `report` names them in
`libraryBundle.refreshed`. Content made for a newer minor version than the pack has plays at the pack's newest older
one, listed in the report's `downgradedLibraries` (player 1.2; `&downgrade-libraries=off` refuses it instead).

H5P's own buttons and dialogs, such as Fullscreen and Rights of use, are in the package's own language,
`defaultLanguage` (or `language`) in its `h5p.json`, so they match the content's text (player 1.1.0). The frame document
takes the same language, for screen readers. `&lang=<tag>` on the address, `de` or `pt-BR`, is the language for a
package that names none. The translations come from the runtime (runtime 0.1.2): more than fifty languages, from
Moodle's language packs, in a script Vite emits as `assets/l10n-*.js` that the frame fetches only for a language other
than English. The bundle's licence list is emitted as
than English. The pack's licence list is emitted as
`assets/libraries-LICENSES.txt`.
`&libraries=` in the query string overrides the sources; `&libraries=none` turns them off.

Expand Down
65 changes: 46 additions & 19 deletions e2e/player.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,28 @@ function watchConsole(page: Page): string[] {
return problems
}

/**
* Collects every request that leaves the origin under test, and any for the whole library pack,
* which this origin no longer serves. The pack's parts come from here and from nowhere else, so a
* library source of whatever name, jsDelivr's copy of the pack, another CDN or the H5P hub, shows
* up as a request to another origin: no list of hosts to keep. The listener is on the context, not
* the page, so that it also hears the Service Worker's own requests, which a page does not.
*/
function watchLeaving(page: Page, baseURL: string | undefined): string[] {
if (!baseURL) throw new Error('playwright.config.ts sets no baseURL')
const own = new URL(baseURL).origin
const leaving: string[] = []
page.context().on('request', (request) => {
const { protocol, origin, pathname } = new URL(request.url())
if (protocol !== 'http:' && protocol !== 'https:') return // data: and blob:
if (origin !== own || /\/libraries-[^/]*\.h5p$/.test(pathname)) leaving.push(request.url())
})
return leaving
}

test('the player page plays a package', async ({ page, baseURL }) => {
const problems = watchConsole(page)
const bundle: string[] = []
page.on('request', (request) => {
if (/\/assets\/libraries-.*\.h5p|api\.h5p\.org/.test(request.url())) bundle.push(request.url())
})
const leaving = watchLeaving(page, baseURL)
await page.goto(`/h5p?src=${baseURL}${SAMPLE}`)

const player = page.locator('h5p-player')
Expand All @@ -38,32 +54,35 @@ test('the player page plays a package', async ({ page, baseURL }) => {
const frame = page.frameLocator('h5p-player iframe').first()
await expect(frame.locator('.h5p-content, .h5p-container').first()).toBeVisible()

// A complete package never costs the library bundle, or a request anywhere else for libraries.
expect(bundle, 'no library source was asked for').toEqual([])
// A complete package costs at most the parts of the libraries it carries at older versions than
// the pack's, from this origin: never the pack whole, and nothing from anywhere else.
expect(leaving, 'nothing left this origin, and the whole pack was not asked for').toEqual([])
expect(problems, problems.join('\n')).toEqual([])
})

test('a package exported without its libraries plays from the bundle', async ({ page, baseURL }) => {
test('a package exported without its libraries plays from the pack served here, part by part', async ({ page, baseURL }) => {
const problems = watchConsole(page)
const bundle: number[] = []
const elsewhere: string[] = []
page.on('response', (response) => {
if (/\/assets\/libraries-.*\.h5p/.test(response.url())) bundle.push(response.status())
})
page.on('request', (request) => {
if (/api\.h5p\.org|cdn\.jsdelivr\.net/.test(request.url())) elsewhere.push(request.url())
const parts: number[] = []
const leaving = watchLeaving(page, baseURL)
// On the context, as in `watchLeaving`, so that a part the Service Worker fetches is counted too.
page.context().on('response', (response) => {
const url = response.url()
if (url.startsWith(`${baseURL}/assets/`) && url.endsWith('.h5p')) parts.push(response.status())
})
// The quiz as H5P.com would export it: `content/` and a manifest that names only the main library.
// It was written for Question Set 1.20 and the pack serves 1.21, so the frame runs the library's
// upgrade steps on it first; a failed upgrade logs a console error, which fails this test.
await page.goto(`/h5p?src=${baseURL}/samples/quiz-without-libraries.h5p`)
const player = page.locator('h5p-player')
// The pack served here, and nothing behind it: the player has no hub to fall back to.
await expect(player).toHaveAttribute('libraries', /^\/assets\/libraries-[^ ]*\.h5p$/)
await expect(player).toHaveAttribute('libraries', 'pack')
await expect.poll(async () => player.evaluate((element: HTMLElement & { state?: string }) => element.state), { timeout: 40_000 }).toBe('ready')
await expect(page.frameLocator('h5p-player iframe').first().locator('.h5p-content, .h5p-container').first()).toBeVisible()
expect(bundle.length, 'the bundle was fetched from this origin').toBeGreaterThan(0)
expect(elsewhere, 'no library source off this origin was asked for').toEqual([])
// The parts the quiz needs, a handful of the pack's hundred and more, each from this origin.
expect(parts.length, 'parts were fetched from this origin').toBeGreaterThan(0)
expect(parts.length, 'only the parts the quiz needs').toBeLessThan(40)
expect(parts.filter((status) => status !== 200), 'every part answered').toEqual([])
expect(leaving, 'nothing left this origin, and the whole pack was not asked for').toEqual([])
expect(problems, problems.join('\n')).toEqual([])
})

Expand Down Expand Up @@ -96,7 +115,7 @@ test('opened on its own, a package from another site plays untrusted, without a
// Played as data: none of the package's own code, its libraries from the pack served here, and
// its content shown only after the runtime's boot script has checked it against their semantics.
expect(await player.evaluate((element: HTMLElement & { untrusted?: boolean }) => element.untrusted)).toBe(true)
await expect(player).toHaveAttribute('libraries', /^\/assets\/libraries-[^ ]*\.h5p$/)
await expect(player).toHaveAttribute('libraries', 'pack')
await expect(page.getByRole('button', { name: 'Open with its own scripts' })).toHaveCount(0)
await expect(page.frameLocator('h5p-player iframe').first().locator('.h5p-content, .h5p-container').first()).toBeVisible()
expect(problems, problems.join('\n')).toEqual([])
Expand Down Expand Up @@ -138,7 +157,15 @@ test('a page that frames it gets a hello, a height and a report', async ({ page,
action: 'report',
source: { type: 'range-http', size: expect.any(Number) },
metadata: { title: 'Do you know what just happened?', license: 'CC0 1.0', authors: ['missing-elements'], mainLibrary: 'H5P.QuestionSet' },
libraryBundle: null,
// The quiz carries its libraries, older than the pack's, so it plays the pack's newer versions
// from this origin (player 0.10) and the report names them; the website says so under its preview.
libraryBundle: {
url: `${baseURL}/assets/`,
origin: baseURL,
fromCache: expect.any(Boolean),
refreshed: expect.arrayContaining([{ library: 'H5P.QuestionSet', from: '1.20.31', to: expect.stringMatching(/^1\.\d+\.\d+$/) }])
},
downgradedLibraries: [],
elapsedMs: expect.any(Number)
})
})
Expand Down
10 changes: 0 additions & 10 deletions src/h5p-embed.d.ts

This file was deleted.

19 changes: 13 additions & 6 deletions src/h5p-page.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import '@missing-elements/h5p-offline-player'
import '@missing-elements/h5p-embed/embed.css'
import { startEmbed } from '@missing-elements/h5p-embed/embed.js'
import libraries from '@missing-elements/h5p-libraries/libraries.h5p?url'
import { pack } from '@missing-elements/h5p-libraries'
import { runtime } from '@missing-elements/h5p-runtime'

/**
Expand All @@ -12,18 +12,25 @@ import { runtime } from '@missing-elements/h5p-runtime'
*
* /h5p?src=<package url>[&frame][&copyright][&export][&icon][&reporting][&fullscreen=off]
* [&xapi=<parent origin>][&activity-id=<IRI>][&custom-css=<stylesheet url>][&lang=<language tag>]
* [&libraries=pack|<url> …|none][&preload=auto]
* [&libraries=pack|<url> …|none][&refresh-libraries=off][&downgrade-libraries=off][&preload=auto]
*
* Options, as this origin needs them:
* - `pack`: the library pack's `pack` export, one part per library, which Vite emits here. A
* package downloads only the parts it needs, each checked against the pack's SHA-256 and kept in
* the browser for every package that uses it, where the whole `libraries.h5p` (15.5 MB since
* pack 1.1.0, 1.8 MB of it libraries the player never reads) was downloaded at the first package
* that lacked anything. A package that carries an older version of a library plays the pack's
* newer one (player 0.10), unless the address says `&refresh-libraries=off`. The build page reads
* the same parts.
* - `defaultLibraries: 'pack'`: an export without its libraries (H5P.com, h5p.org) takes them from
* the pack served here, and from nowhere else: the player has no hub fallback since 0.6, so the
* page asks no third party for anything. Snippets have never carried `&libraries=`, so this keeps
* them playing such exports.
* that pack, and from nowhere else: the player has no hub fallback since 0.6, so the page asks no
* third party for anything. Snippets have never carried `&libraries=`, so this keeps them playing
* such exports.
* - `packages` left out: a package from any host plays, which a public tool needs.
* - `askInOwnFrame: false`: a page of this origin that frames it gets the package as it is, not
* `untrusted`; nothing on this origin frames it except the browser tests. Framed by another site,
* the storage is that site's own, and a package always plays as it is. Opened on its own, a
* package from another site plays `untrusted` (h5p-embed 0.4): none of its own code runs, and
* since the runtime's 0.2 its content is checked against its libraries' semantics first.
*/
startEmbed({ librariesPack: libraries, defaultLibraries: 'pack', runtime, askInOwnFrame: false })
startEmbed({ pack, defaultLibraries: 'pack', runtime, askInOwnFrame: false })
1 change: 0 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"src/h5p-page.ts",
"src/build-page.ts",
"src/build-source.ts",
"src/h5p-embed.d.ts",
"e2e",
"playwright.config.ts",
"vite.config.ts"
Expand Down
Loading