Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

APIs for shimming standard APIs #956

Merged
merged 36 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
23872cd
Make defineProperty() strict to prevent descriptor mismatch
muodov Apr 22, 2024
2425160
First version of interface shimming API
muodov Apr 22, 2024
6e21078
first version of shimProperty()
muodov Apr 22, 2024
9a77d77
Fix toString wrapping in defineProperty() and handle toString.toString()
muodov Apr 23, 2024
ca3e1ed
Set up browser-based unit tests
muodov Apr 23, 2024
2bc30ae
Fix toString wrapper to work with already wrapped objects
muodov Apr 23, 2024
20d07b6
handle toString on shim class objects
muodov Apr 23, 2024
5eeb8c0
Handle toString on shim interface methods
muodov Apr 23, 2024
e19f7ae
Add tests for toString() wrapping
muodov Apr 23, 2024
eca5f5a
Mark shim classes
muodov Apr 24, 2024
fcf9b3b
mock the name property on shim classes
muodov Apr 24, 2024
95c22a7
API for shimming standard properties
muodov Apr 24, 2024
76a8510
Rename test file
muodov Apr 24, 2024
900f887
Minor
muodov Apr 24, 2024
5678d5e
Update src/content-feature.js
muodov Apr 25, 2024
bef5a56
Make toString wrapper less confusing
muodov Apr 25, 2024
2d938ad
Fix existing incomplete property descriptors
muodov Apr 25, 2024
102e396
Tiny lint fix
muodov Apr 25, 2024
c2ba146
Remove unused wrapConstructor
muodov Apr 25, 2024
923989b
Move shim implementation to wrapper-utils and convert types to jsdoc
muodov Apr 25, 2024
3b5214a
Split defineProperty wrapper from debug flag
muodov Apr 25, 2024
f12eff3
Lint fix in DDGProxy
muodov Apr 25, 2024
3ad47c3
Lint fix
muodov Apr 26, 2024
831294f
Fix file size tests
muodov Apr 26, 2024
693ff68
Add tet utils for webcompat shims
muodov Apr 28, 2024
4345985
Use shim API for the Presentation fix
muodov Apr 28, 2024
a3682f9
Fix descriptor properties for Notification
muodov Apr 29, 2024
34a020a
Merge branch 'main' into max/shim-apis
muodov Apr 29, 2024
feb7461
minor
muodov Apr 30, 2024
065ab7b
Convert shim API tests from WTR to Jasmine
muodov Apr 30, 2024
a61cb2b
Move out test pages logic
muodov May 1, 2024
9e4e014
Convert shim correctness tests to puppeteer
muodov May 1, 2024
2d4543b
Add some readme docs for the shim APIs
muodov May 14, 2024
a9ca257
Merge branch 'main' into max/shim-apis
muodov May 16, 2024
4cc8705
Apply shimMark only in test mode
muodov May 16, 2024
e3a5c5f
Fix unit test
muodov May 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ To handle the difference in scope injection we expose multiple utilities which b
return await nativeImpl.call(this, queryObject)
})
```
- `ContentFeature.shimInterface(interfaceName, ImplClass, options)`
- API for shimming standard constructors. See the WebCompat feature and JSDoc for more details.
- Example usage:
```javascript
this.shimInterface('MediaSession', MyMediaSessionClass, {
disallowConstructor: true,
allowConstructorCall: false,
wrapToString: true
})
```
- `ContentFeature.shimProperty(instanceHost, instanceProp, implInstance, readOnly = false)`
- API for shimming standard global objects. Usually you want to call `shimInterface()` first, and pass an object instance as `implInstance`. See the WebCompat feature and JSDoc for more details.
- Example usage:
```javascript
this.shimProperty(Navigator.prototype, 'mediaSession', myMediaSessionInstance, true)
```

- `DDGProxy`
- Behaves a lot like `new window.Proxy` with a few differences:
- has an `overload` method to actually apply the function to the native property.
Expand Down
105 changes: 61 additions & 44 deletions integration-test/test-pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,57 @@ describe('Test integration pages', () => {
await teardown()
})

it('Should be successful page script check', async () => {
/**
* @param {string} pageName
* @param {string} configPath
* @param {string} [evalBeforeInit]
*/
async function testPage (pageName, configPath, evalBeforeInit) {
const port = server.address().port
const page = await browser.newPage()
const res = fs.readFileSync(configPath)
// @ts-expect-error - JSON.parse returns any
const config = JSON.parse(res)
polyfillProcessGlobals()

/** @type {import('../src/utils.js').UserPreferences} */
const userPreferences = {
platform: {
name: 'extension'
},
sessionKey: 'test'
}
const processedConfig = processConfig(config, /* userList */ [], /* preferences */ userPreferences/*, platformSpecificFeatures = [] */)

await gotoAndWait(page, `http://localhost:${port}/${pageName}?automation=true`, processedConfig, evalBeforeInit)
// Check page results
const pageResults = await page.evaluate(
() => {
let res
const promise = new Promise(resolve => {
res = resolve
})
// @ts-expect-error - results is not defined in the type definition
if (window.results) {
// @ts-expect-error - results is not defined in the type definition
res(window.results)
} else {
window.addEventListener('results-ready', (e) => {
// @ts-expect-error - e.detail is not defined in the type definition
res(e.detail)
})
}
return promise
}
)
for (const key in pageResults) {
for (const result of pageResults[key]) {
expect(result.result).withContext(key + ':\n ' + result.name).toEqual(result.expected)
}
}
}

describe('Runtime checks', () => {
const pages = {
'runtime-checks/pages/basic-run.html': 'runtime-checks/config/basic-run.json',
'runtime-checks/pages/replace-element.html': 'runtime-checks/config/replace-element.json',
Expand All @@ -32,49 +82,16 @@ describe('Test integration pages', () => {
}
for (const pageName in pages) {
const configName = pages[pageName]

const port = server.address().port
const page = await browser.newPage()
const res = fs.readFileSync(process.cwd() + '/integration-test/test-pages/' + configName)
// @ts-expect-error - JSON.parse returns any
const config = JSON.parse(res)
polyfillProcessGlobals()

/** @type {import('../src/utils.js').UserPreferences} */
const userPreferences = {
platform: {
name: 'extension'
},
sessionKey: 'test'
}
const processedConfig = processConfig(config, /* userList */ [], /* preferences */ userPreferences/*, platformSpecificFeatures = [] */)

await gotoAndWait(page, `http://localhost:${port}/${pageName}?automation=true`, processedConfig)
// Check page results
const pageResults = await page.evaluate(
() => {
let res
const promise = new Promise(resolve => {
res = resolve
})
// @ts-expect-error - results is not defined in the type definition
if (window.results) {
// @ts-expect-error - results is not defined in the type definition
res(window.results)
} else {
window.addEventListener('results-ready', (e) => {
// @ts-expect-error - e.detail is not defined in the type definition
res(e.detail)
})
}
return promise
}
)
for (const key in pageResults) {
for (const result of pageResults[key]) {
expect(result.result).withContext(key + ':\n ' + result.name).toEqual(result.expected)
}
}
it(`${pageName}`, async () => {
await testPage(pageName, process.cwd() + '/integration-test/test-pages/' + configName)
})
}
})

it('Web compat shims correctness', async () => {
await testPage(
'webcompat/pages/shims.html',
`${process.cwd()}/integration-test/test-pages/webcompat/config/shims.json`
)
})
})
11 changes: 11 additions & 0 deletions integration-test/test-pages/webcompat/config/shims.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"features": {
"webCompat": {
"state": "enabled",
"settings": {
"mediaSession": "enabled",
"presentation": "enabled"
}
}
}
}
129 changes: 129 additions & 0 deletions integration-test/test-pages/webcompat/pages/shims.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Webcompat shims</title>
<link rel="stylesheet" href="../../shared/style.css">
</head>
<body>
<script src="../../shared/utils.js"></script>
<p><a href="../index.html">[Webcompat shims]</a></p>

<p>This page verifies that shims created with shim APIs have correct descriptors</p>

<script>
/**
* assert that two descriptors are similar, allowing different values
* @param {import('../src/wrapper-utils').StrictPropertyDescriptor} origDesc
* @param {import('../src/wrapper-utils').StrictPropertyDescriptor} newDesc
* @returns {string} - the error message if the test fails
*/
function compareDescriptorShape (origDesc, newDesc) {
const origKeys = Object.keys(origDesc)
const newKeys = Object.keys(newDesc)
// verify that both descriptors have the same keys
if (origKeys.sort().join(',') !== newKeys.sort().join(','))
return 'property keys do not match';

for (const key of origKeys) {
if (key === 'get' || key === 'set' || key === 'value') {
if (typeof newDesc[key] !== typeof origDesc[key])
return `property ${key} does not match`;
} else {
if (newDesc[key] !== origDesc[key])
return `property ${key} does not match`;
}
}
}

/**
* Use this function to test the interfaces shimmed in the web-compat feature
* @param {string} interfaceName - the name of the interface to test. It should be available in the global scope
* @param {import('../src/wrapper-utils').StrictPropertyDescriptor} origInterfaceDescriptor - the descriptor of the original interface
* @returns {string} - the error message if the test fails
*/
function testInterfaceShimCorrectness (interfaceName, origInterfaceDescriptor) {
if (!interfaceName) {
return 'Nothing to test.';
}

if (!globalThis[interfaceName])
return 'native class is not found after shimming';
if (globalThis[interfaceName][globalThis.ddgShimMark] !== true)
return 'class should be marked as shimmed';

const newInterfaceDescriptor = Object.getOwnPropertyDescriptor(globalThis, interfaceName)

return compareDescriptorShape(origInterfaceDescriptor, newInterfaceDescriptor)
}

/**
* Use this function to test the global properties shimmed in the web-compat feature
* @param {any} instanceHost - object under which the global instance is defined
* @param {string} instanceProp - the name of the instance property
* @param {import('../src/wrapper-utils').StrictPropertyDescriptor} origInstanceDescriptor - the descriptor of the original instance property
* @returns {string} - the error message if the test fails
*/
function testInstanceShimCorrectness (instanceHost, instanceProp, origInstanceDescriptor) {
if (!instanceHost || !instanceProp) {
return 'Nothing to test.';
}

if (!instanceHost[instanceProp])
return 'global instance is not found after shimming';

const newInstanceDescriptor = Object.getOwnPropertyDescriptor(instanceHost, instanceProp)

return compareDescriptorShape(origInstanceDescriptor, newInstanceDescriptor)
}


test('Interface shims', async () => {
const results = [];
results.push({
name: 'origInterfaceDescriptors found',
result: Boolean(globalThis.origInterfaceDescriptors),
expected: true
});
results.push({
name: 'ddgShimMark found',
result: Boolean(globalThis.ddgShimMark),
expected: true
});

for (const [interfaceName, origDescriptor] of Object.entries(globalThis.origInterfaceDescriptors)) {
results.push({
name: `${interfaceName}'s descriptor is correct`,
result: testInterfaceShimCorrectness(interfaceName, origDescriptor),
expected: undefined
});
}

return results;
});

test('Instance shims', async () => {
const results = [];
results.push({
name: 'origPropDescriptors found',
result: Boolean(globalThis.origPropDescriptors),
expected: true
});

for (const [instanceHost, instanceProp, origDescriptor] of globalThis.origPropDescriptors) {
results.push({
name: `${instanceHost}.${instanceProp}'s descriptor is correct`,
result: testInstanceShimCorrectness(instanceHost, instanceProp, origDescriptor),
expected: undefined
});
}

return results;
});

// eslint-disable-next-line no-undef
renderResults();
</script>
</body>
</html>