Skip to content
Open
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
57 changes: 56 additions & 1 deletion assertions.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import assert from "node:assert/strict";
import { assertValidFeatureReference } from "./assertions";
import {
assertAllowedOverlap,
assertValidFeatureReference,
} from "./assertions";

describe("assertValidReference()", function () {
it("throws if target ID is a move", function () {
Expand Down Expand Up @@ -34,3 +37,55 @@ describe("assertValidReference()", function () {
});
});
});

describe("assertAllowedOverlap()", function () {
it("does not throw when a key does not overlap", function () {
assert.doesNotThrow(() => {
const keysToIDs = new Map([["api.HTMLMediaElement", ["audio"]]]);
assertAllowedOverlap(
`api.HTMLMediaElement`,
`audio`,
keysToIDs,
new Map(),
);
});
});

it("does not throw when a key is allowlisted with another feature", function () {
assert.doesNotThrow(() => {
const keysToIDs = new Map([["api.HTMLMediaElement", ["audio", "video"]]]);
assertAllowedOverlap(
"api.HTMLMediaElement",
"audio",
keysToIDs,
new Map([["api.HTMLMediaElement", ["audio", "video"]]]),
);
});
});

it("throws when a key is not allowlisted and overlaps", function () {
assert.throws(() => {
const keysToIDs = new Map([["api.HTMLMediaElement", ["audio", "video"]]]);
assertAllowedOverlap(
"api.HTMLMediaElement",
"audio",
keysToIDs,
new Map(),
);
});
});

it("throws when a key is allowlisted but overlaps with an unnamed feature", function () {
assert.throws(() => {
const keysToIDs = new Map([
["api.HTMLMediaElement", ["audio", "media-super-feature"]],
]);
assertAllowedOverlap(
"api.HTMLMediaElement",
"video",
keysToIDs,
new Map([["api.HTMLMediaElement", ["audio", "media-super-feature"]]]),
);
});
});
});
21 changes: 21 additions & 0 deletions assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,25 @@ export function assertRequiredRemovalDateSet(
);
}

// compat keys to overlapping feature ID list
export type OverlapAllowlist = Map<string, string[]>;

export function assertAllowedOverlap(
compatKey: string,
featureID: string,
compatKeysToIDs: Map<string, string[]>,
allowlist: OverlapAllowlist,
) {
const featuresWithThisCompatKey = compatKeysToIDs.get(compatKey) ?? [];
if (featuresWithThisCompatKey.length === 1) {
return;
}
const overlappers = allowlist.get(compatKey) ?? [];
if (!overlappers.includes(featureID)) {
throw new Error(
`BCD key ${compatKey} overlaps with one or more other features (${featuresWithThisCompatKey.join(", ")}) but the feature is not allowlisted (${overlappers.join(", ")})`,
);
}
}

// TODO: assertValidSnapshotReference
32 changes: 32 additions & 0 deletions docs/guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,3 +423,35 @@ When you set a `discouraged` block in a feature file, do:
- Set one or more (optional) `alternatives` feature IDs that are whole or partial substitutes for the discouraged feature.
An alternative doesn't have to be a narrow drop-in replacement for the discouraged feature but it must handle some use case of the discouraged feature.
Guide developers to the most relevant features that would help them stop using the discouraged feature.

## Overlapping `compat_features` keys

Typically, an `@mdn/browser-compat-data` (BCD) key is assigned to one and only one feature.
Rarely, two or more features have equal claim to a key and their sets of `compat_features` keys must overlap.
You can add a single key to two or more `compat_features` lists when independent features share some interface and one of these situations applies:

- No feature has any claim to being first and the shared interface does not represent a useful feature on its own.
This sometimes happens when two independent features share a common interface.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think such features are very rarely independent, and more commonly are closely related:

Suggested change
This sometimes happens when two independent features share a common interface.
This sometimes happens when two features share a common interface.

For example, the `HTMLMediaElement` interface is required for both the `<video>` and `<audio>` elements, but there's no application for the `HTMLMediaElement` alone.

- The computed status would be misleading without the shared interface.
For example, features where the `Reporting-Endpoints` header replaced the now-deprecated `Report-To` header might have a deceptively early status without the key for `Reporting-Endpoints`.

Do not overlap keys for completeness or relatedness alone.
Instead, prefer to assign such keys to the oldest, most foundational feature where that key might plausibly belong.
For example, do not add `api.HTMLElement` to every HTML element feature (for example, `<div>`, `<span>`, and so on).
Instead, assign `api.HTMLElement` to the DOM feature.

Overlaps are forbidden by default and cause an error.
If you must overlap a key between two features, then first exempt the relevant keys in [`features/_overlap_allowlist.yml`](../features/_overlap_allowlist.yml).
The allowlist is an array of records that declare which features are to share some keys and which keys those features are allowed to share.
Here's an example, where two features share two keys:

```
- features:
- audio
- video
keys:
- api.HTMLMediaElement
- api.HTMLMediaElement.abort_event
```
54 changes: 54 additions & 0 deletions features/_overlap_allowlist.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
- features:
- audio
- video
keys:
- api.HTMLMediaElement
- api.HTMLMediaElement.abort_event
- api.HTMLMediaElement.autoplay
- api.HTMLMediaElement.buffered
- api.HTMLMediaElement.canplay_event
- api.HTMLMediaElement.canplaythrough_event
- api.HTMLMediaElement.canPlayType
- api.HTMLMediaElement.controls
- api.HTMLMediaElement.crossOrigin
- api.HTMLMediaElement.currentSrc
- api.HTMLMediaElement.currentTime
- api.HTMLMediaElement.defaultMuted
- api.HTMLMediaElement.defaultPlaybackRate
- api.HTMLMediaElement.duration
- api.HTMLMediaElement.durationchange_event
- api.HTMLMediaElement.emptied_event
- api.HTMLMediaElement.ended
- api.HTMLMediaElement.ended_event
- api.HTMLMediaElement.error
- api.HTMLMediaElement.error_event
- api.HTMLMediaElement.load
- api.HTMLMediaElement.loadeddata_event
- api.HTMLMediaElement.loadedmetadata_event
- api.HTMLMediaElement.loadstart_event
- api.HTMLMediaElement.loop
- api.HTMLMediaElement.muted
- api.HTMLMediaElement.networkState
- api.HTMLMediaElement.pause
- api.HTMLMediaElement.pause_event
- api.HTMLMediaElement.paused
- api.HTMLMediaElement.play
- api.HTMLMediaElement.play_event
- api.HTMLMediaElement.play.returns_promise
- api.HTMLMediaElement.playbackRate
- api.HTMLMediaElement.played
- api.HTMLMediaElement.playing_event
- api.HTMLMediaElement.preload
- api.HTMLMediaElement.progress_event
- api.HTMLMediaElement.ratechange_event
- api.HTMLMediaElement.readyState
- api.HTMLMediaElement.seekable
- api.HTMLMediaElement.seeked_event
- api.HTMLMediaElement.seeking
- api.HTMLMediaElement.seeking_event
- api.HTMLMediaElement.src
- api.HTMLMediaElement.stalled_event
- api.HTMLMediaElement.suspend_event
- api.HTMLMediaElement.timeupdate_event
- api.HTMLMediaElement.volumechange_event
- api.HTMLMediaElement.waiting_event
63 changes: 61 additions & 2 deletions features/audio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,64 @@ group:
caniuse: audio
status:
compute_from: html.elements.audio
# TODO: Tag relevant parts of HTMLMediaElement when possible:
# https://github.com/web-platform-dx/web-features/issues/1173
compat_features:
- api.HTMLMediaElement
- api.HTMLMediaElement.abort_event
- api.HTMLMediaElement.autoplay
- api.HTMLMediaElement.buffered
- api.HTMLMediaElement.canplay_event
- api.HTMLMediaElement.canplaythrough_event
- api.HTMLMediaElement.canPlayType
- api.HTMLMediaElement.controls
- api.HTMLMediaElement.crossOrigin
- api.HTMLMediaElement.currentSrc
- api.HTMLMediaElement.currentTime
- api.HTMLMediaElement.defaultMuted
- api.HTMLMediaElement.defaultPlaybackRate
- api.HTMLMediaElement.duration
- api.HTMLMediaElement.durationchange_event
- api.HTMLMediaElement.emptied_event
- api.HTMLMediaElement.ended
- api.HTMLMediaElement.ended_event
- api.HTMLMediaElement.error
- api.HTMLMediaElement.error_event
- api.HTMLMediaElement.load
- api.HTMLMediaElement.loadeddata_event
- api.HTMLMediaElement.loadedmetadata_event
- api.HTMLMediaElement.loadstart_event
- api.HTMLMediaElement.loop
- api.HTMLMediaElement.muted
- api.HTMLMediaElement.networkState
- api.HTMLMediaElement.pause
- api.HTMLMediaElement.pause_event
- api.HTMLMediaElement.paused
- api.HTMLMediaElement.play
- api.HTMLMediaElement.play_event
- api.HTMLMediaElement.play.returns_promise
- api.HTMLMediaElement.playbackRate
- api.HTMLMediaElement.played
- api.HTMLMediaElement.playing_event
- api.HTMLMediaElement.preload
- api.HTMLMediaElement.progress_event
- api.HTMLMediaElement.ratechange_event
- api.HTMLMediaElement.readyState
- api.HTMLMediaElement.seekable
- api.HTMLMediaElement.seeked_event
- api.HTMLMediaElement.seeking
- api.HTMLMediaElement.seeking_event
- api.HTMLMediaElement.src
- api.HTMLMediaElement.stalled_event
- api.HTMLMediaElement.suspend_event
- api.HTMLMediaElement.timeupdate_event
- api.HTMLMediaElement.volumechange_event
- api.HTMLMediaElement.waiting_event
- api.HTMLAudioElement
- api.HTMLAudioElement.Audio
- html.elements.audio
- html.elements.audio.autoplay
- html.elements.audio.controls
- html.elements.audio.crossorigin
- html.elements.audio.loop
- html.elements.audio.muted
- html.elements.audio.preload
- html.elements.audio.src
Loading