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
37 changes: 37 additions & 0 deletions .github/workflows/pr_web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,40 @@ jobs:

- name: Test
run: npm test

e2e:
name: E2E tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'

- name: Install dependencies
run: npm ci --workspaces --include-workspace-root

- name: Build workspaces
run: npm run build

- name: Install & update e2e app with latest react-native-theoplayer
working-directory: apps/e2e
run: npm install react-native-theoplayer@latest

- name: Run e2e tests
working-directory: apps/e2e
env:
E2E_HEADLESS: 'true'
run: npm run test:e2e:web -- --markdown

- name: Summarize results
working-directory: apps/e2e
if: always()
run: |
if [ -f cavynext_results.md ]; then
cat cavynext_results.md >> $GITHUB_STEP_SUMMARY
fi
133 changes: 133 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# AGENTS.md

Guidance for coding agents working in this repository. Keep it short and factual; if something here
turns out to be wrong, fix this file in the same PR.

## What this repo is

An npm-workspaces monorepo of connectors for
[`react-native-theoplayer`](https://github.com/THEOplayer/react-native-theoplayer): analytics
integrations (`adobe`, `adobe-edge`, `adscript`, `agama`, `bitmovin`, `comscore`, `conviva`,
`gemius`, `mux`, `nielsen`, `npaw`, `youbora`), plus `drm`, `engage` and `yospace`. Each top-level
directory is a published package; `apps/e2e` is the React Native test app and `apps/engage-example`
a sample app.

Each connector typically has:

- `src/api` — public API re-exported from `src/index.ts` (this is what typedoc documents).
- `src/internal` — implementation, split per platform: a shared `*Adapter.ts` interface with
`*AdapterNative.ts` (Android/iOS bridge) and `*AdapterWeb.ts` (browser SDK) implementations, the
latter usually under `src/internal/web`.
- `android/` (Kotlin/Java), `ios/` + `*.podspec` (Swift/Obj-C) for native connectors.
- Build via `react-native-builder-bob` (`npm run build` → `lib/`); `npm run prepare` also
regenerates `src/manifest.json`.

Node 22 is used in CI.

## Commands

Run from the repo root unless stated otherwise.

| Task | Command | Notes |
| --- | --- | --- |
| Install | `npm ci --workspaces --include-workspace-root` | |
| Build all packages | `npm run build` | bob build per workspace |
| Docs check ("root tests") | `npm test` | alias for `npm run test:docs` → typedoc with `--treatWarningsAsErrors`; **not** a unit-test run |
| Lint | `npm run lint` / `npm run lint:fix` | eslint over `*/{src,test}` |
| Format | `npm run prettier` / `npm run prettier:fix` | prettier `--check`; single quotes, trailing commas, print width 150 |
| Unit tests (per package) | `cd adobe-edge && npm test` | jest + ts-jest; see below |
| Typecheck (per package) | `cd adobe-edge && npm run typescript` | `tsc --noEmit` |
| E2E app | `cd apps/e2e && npm run test:e2e:android` / `test:e2e:ios` / `test:e2e:tvos` | CavyNext, needs an emulator/simulator |
| E2E app (web) | `cd apps/e2e && npm run test:e2e:web` | webpack dev server + CavyNext; opens your default browser. Set `E2E_HEADLESS=true` for headless Chrome (what CI does) |

Gotchas worth knowing before you claim "tests pass":

- **The root `npm test` is a documentation check, not a test suite.** It fails on typedoc warnings
(e.g. an exported symbol that is not documented or not reachable from `src/index.ts`).
- **Unit tests exist in only some packages.** At the time of writing, `adobe-edge` is the only
workspace with a `test` script (`jest.config.js` + `src/**/__tests__`). Adding tests to other
connectors is welcome; copy the `adobe-edge` setup.
- **`npm run prettier` at the root is currently red on pre-existing files** under `comscore/`,
`drm/` and `engage/`. Do not reformat those as a side effect of an unrelated change; only ensure
the files you touched are formatted (the commit hook does this for staged files via
husky + lint-staged).
- `npm run lint` currently reports warnings (e.g. `@typescript-eslint/no-explicit-any`,
unused vars). Errors must be zero; do not mass-fix unrelated warnings in a feature PR.

## What CI actually verifies

- `pr_web.yml`: install, `npm run build`, `npm test` (docs check), plus the CavyNext e2e suite in
headless Chrome (`E2E_HEADLESS=true npm run test:e2e:web`). No jest, no tsc per package, no lint,
no prettier.
- `pr_android.yml` / `pr_ios.yml`: install, build, docs check, then build the `apps/e2e` app against
`react-native-theoplayer@latest` and run the CavyNext e2e suite on an emulator/simulator
(Android API 36; iOS + tvOS on Xcode 26.2). These are skipped for draft PRs.
- `release.yml` (on `main`): changesets action opens/updates a "Release" PR or publishes to npm.

Consequence for agents: green CI does **not** mean the package's unit tests or typecheck passed. Run
`npm test` and `npm run typescript` inside the package you changed, locally, and say so in the PR.

## Conventions

- **Changesets are required** for any user-facing change: add a markdown file under `.changeset/`
naming the affected package(s) and a semver bump, e.g.

```md
---
'@theoplayer/react-native-analytics-adobe-edge': patch
---

Fixed <what the user observes>, which happened because <cause>.
```

No changeset means the fix ships without a version bump or changelog entry. Internal-only changes
(tests, CI, docs) don't need one.
- **Player/SDK version support** is expressed through peerDependencies, typically
`react-native-theoplayer: ^10 || ^11` and `theoplayer: ^10 || ^11` (THEOplayer 10/11 are branded
OptiView). Don't tighten or widen these ranges casually — it is a breaking change for consumers.
- **Public API changes must stay documented**: everything exported from `src/index.ts` is fed to
typedoc with warnings-as-errors, so add TSDoc for new public symbols or the docs check fails.
- Keep platform-specific code behind the adapter split rather than branching on `Platform.OS` in
shared code.
- Commit hooks (`.husky/pre-commit` → `lint-staged`) run prettier on staged files. Don't bypass them
with `--no-verify`.

## E2E app on web

`apps/e2e` runs on react-native-web through webpack (`web/webpack.config.js`). Things that are easy
to get wrong:

- CavyNext only accepts entry files ending in `.js`, and swaps `index.web.js` for
`index.test.web.js` while running (restoring it on exit). Keep both as thin wrappers around
`web/app.web.tsx` / `web/test.web.tsx`; if a run is interrupted you may find a leftover
`index.notest.js` that has to be renamed back.
- THEOplayer worker/iframe assets are copied to `libraryLocation` from the `theoplayer` package
resolved **from `apps/e2e`**, not from the workspace root: the app can hoist a different version,
and mismatched workers fail at runtime with "THEOplayer workers could not be loaded".
- The web player needs `mutedAutoplay` and a `libraryLocation` to autoplay in a headless browser;
see `src/TestableApp.tsx`.
- Headless runs use a dedicated Chrome profile so they don't join an already running browser —
a second window in an existing instance shows up as "an extra app connection" and the run hangs.

## Unit tests (adobe-edge, as the reference setup)

`adobe-edge/src/internal/web/__tests__` shows the intended pattern for web-connector tests:

- `mocks/alloy.ts` mocks `@adobe/alloy`'s `createInstance`/media tracker; `mocks/player.ts` provides
a minimal event-emitting player stub, so tests drive behaviour by emitting player events
(`loadedmetadata`, `sourcechange`, …).
- Async edge SDK behaviour is driven with explicit deferreds plus a `flushMicrotasks()` helper
(a `MessageChannel` round-trip) and `jest.useFakeTimers()` /
`jest.advanceTimersByTimeAsync()` for retry/backoff logic.
- Mock implementations returning heterogeneous promises usually need an explicit `: any` return
annotation to satisfy `tsc --noEmit`.

Third-party SDK behaviour is best verified against the pinned dependency in `node_modules` (e.g.
`node_modules/@adobe/alloy/dist/alloy.js`) rather than from memory — vendor docs and the shipped
implementation do diverge.

## PRs

- Reference the customer-visible symptom, not just the code change, and include the changeset.
- State which package-level checks you ran (`npm test`, `npm run typescript`) since CI won't.
- Don't include unrelated formatting churn.
3 changes: 3 additions & 0 deletions apps/e2e/index.test.web.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Web entry point used by `npm run test:e2e:web`; cavynext temporarily swaps it
// in for `index.web.js`. See index.web.js.
import './web/test.web';
6 changes: 6 additions & 0 deletions apps/e2e/index.web.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Web entry point of the app.
//
// This file is intentionally a thin, JSX-free wrapper: cavynext swaps
// `index.web.js` and `index.test.web.js` to boot the app into its test runner,
// and it only accepts entry files ending in `.js`.
import './web/app.web';
1 change: 1 addition & 0 deletions apps/e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"test:e2e:android": "npx cavynext run-android --terminal bash --mode release",
"test:e2e:ios": "npx cavynext run-ios --terminal bash --scheme ReactNativeTHEOplayer",
"test:e2e:tvos": "npx cavynext run-ios --terminal bash --scheme ReactNativeTHEOplayer-tvOS",
"test:e2e:web": "npx cavynext run-web --boot-timeout 10 --buildCmd \"npx webpack serve --mode development --config web/webpack.config.js\"",
Comment thread
tvanlaerhoven marked this conversation as resolved.
"android": "npx react-native run-android",
"android-release": "npx react-native run-android --mode release",
"ios": "npx react-native run-ios",
Expand Down
14 changes: 13 additions & 1 deletion apps/e2e/src/TestableApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ import { Platform, SafeAreaView, StyleSheet, View, ViewStyle } from 'react-nativ
import { TestableTHEOplayerView } from './components/TestableTHEOplayerView';
import Specs from './tests';
import { getStatusBarHeight } from 'react-native-status-bar-height';
import type { PlayerConfiguration } from 'react-native-theoplayer';
import Config from 'react-native-config';

// On web, the transmuxer worker files are served from `libraryLocation` and
// autoplay is only allowed while muted; both are needed to play the test
// sources. The license is optional: the test sources are demo sources hosted on
// a '*.theoplayer.com' domain, which play without one.
const PLAYER_CONFIG: PlayerConfiguration = {
license: Config.THEO_LICENSE_KEY,
libraryLocation: 'theoplayer',
mutedAutoplay: 'all',
};

const testHookStore = new TestHookStore();
// Debug simulators need extra time for cold-start native player creation.
Expand All @@ -30,7 +42,7 @@ export class TestableApp extends Component {
<Tester specs={Specs} store={testHookStore} waitTime={TESTER_WAIT_TIME}>
<SafeAreaView style={[StyleSheet.absoluteFill, { backgroundColor: '#000000' }]}>
<View style={PLAYER_CONTAINER_STYLE}>
<TestableTHEOplayerView />
<TestableTHEOplayerView config={PLAYER_CONFIG} />
</View>
</SafeAreaView>
</Tester>
Expand Down
2 changes: 1 addition & 1 deletion apps/e2e/index.web.tsx → apps/e2e/web/app.web.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './src/App';
import App from '../src/App';

createRoot(document.getElementById('app') as HTMLElement).render(
<StrictMode>
Expand Down
6 changes: 6 additions & 0 deletions apps/e2e/web/test.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { TestableApp } from '../src/TestableApp';

// Deliberately not wrapped in <StrictMode>, for the reasons documented on TestableApp.
createRoot(document.getElementById('app') as HTMLElement).render(<TestableApp />);
45 changes: 38 additions & 7 deletions apps/e2e/web/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires,no-undef */
const os = require('os');
const path = require('path');
const webpack = require('webpack');
const dotenv = require('dotenv');
Expand All @@ -9,6 +10,10 @@ const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
const workspaceDirectory = path.resolve(__dirname, '../../..');
const appDirectory = path.resolve(__dirname, '..');

// Resolve theoplayer from the app itself: the app can hoist a different version
// than the workspace root, and its worker files must match the bundled player.
const theoplayerDirectory = path.dirname(require.resolve('theoplayer/package.json', { paths: [appDirectory] }));

// A folder for any stub components we need in case there is no counterpart for it on react-native-web.
const stubDirectory = path.resolve(appDirectory, './web/stub/');

Expand All @@ -24,11 +29,12 @@ const libraryLocation = 'theoplayer';
// Webpack's output location
const outputLocation = 'dist';

// Prepare env keys
// Prepare env keys. The .env file is optional: connector credentials are only
// needed to talk to real back-ends, not to build or to run the e2e suite.
const envPath = path.resolve(appDirectory, '.env');
const env = dotenv.parse(fs.readFileSync(envPath));
const env = fs.existsSync(envPath) ? dotenv.parse(fs.readFileSync(envPath)) : {};
const envKeys = {
'GLOBAL_ENV': `{${Object.entries(env)
GLOBAL_ENV: `{${Object.entries(env)
.map(([key, value]) => `${JSON.stringify(key)}:${JSON.stringify(value)}`)
.join(',')}}`,
};
Expand All @@ -38,13 +44,18 @@ const CopyWebpackPluginConfig = new CopyWebpackPlugin({
{
// Copy transmuxer worker files.
// THEOplayer will find them by setting `libraryLocation` in the playerConfiguration.
from: path.resolve(workspaceDirectory, './node_modules/theoplayer/THEOplayer.transmux.*').replace(/\\/g, '/'),
from: path.resolve(theoplayerDirectory, './THEOplayer.transmux.*').replace(/\\/g, '/'),
to: `${libraryLocation}/[name][ext]`,
},
{
// Copy service worker
// THEOplayer will find them by setting `libraryLocation` in the playerConfiguration.
from: path.resolve(workspaceDirectory, './node_modules/theoplayer/theoplayer.sw.js').replace(/\\/g, '/'),
from: path.resolve(theoplayerDirectory, './theoplayer.sw.js').replace(/\\/g, '/'),
to: `${libraryLocation}/[name][ext]`,
},
{
// Copy the iframe helper page, loaded from `libraryLocation`.
from: path.resolve(theoplayerDirectory, './iframe.html').replace(/\\/g, '/'),
to: `${libraryLocation}/[name][ext]`,
},
{
Expand Down Expand Up @@ -91,7 +102,7 @@ module.exports = {
// load any web API polyfills
// path.resolve(appDirectory, 'polyfills-web.js'),
// your web-specific entry file
path.resolve(appDirectory, 'index.web.tsx'),
path.resolve(appDirectory, 'index.web.js'),
],

// configures where the build ends up
Expand Down Expand Up @@ -122,7 +133,27 @@ module.exports = {
plugins: [HTMLWebpackPluginConfig, CopyWebpackPluginConfig, new NodePolyfillPlugin(), new webpack.DefinePlugin(envKeys)],
devServer: {
// Tells dev-server to open the browser after server had been started.
open: true,
// With E2E_HEADLESS=true (CI) a headless Chrome is opened instead of the
// default browser, so the cavynext suite can run without a desktop.
open:
process.env.E2E_HEADLESS === 'true'
? {
app: {
name: process.env.E2E_BROWSER || 'google-chrome',
arguments: [
'--headless=new',
'--no-sandbox',
'--disable-dev-shm-usage',
'--autoplay-policy=no-user-gesture-required',
'--mute-audio',
// A dedicated profile, so this run neither joins nor disturbs an
// already running browser: a second window in an existing
// instance would report to cavynext as a duplicate app.
`--user-data-dir=${path.join(os.tmpdir(), 'theoplayer-e2e-web-profile')}`,
],
},
}
: true,
historyApiFallback: true,
static: [
{
Expand Down
Loading