Skip to content

Commit db1a0a6

Browse files
Add API defining behaviour to apiManipulation (#1771)
* Add API defining behaviour to apiManipulation * Simplify unit test * Fix linting * Pass correct params in unit test
1 parent 4a74a0f commit db1a0a6

File tree

4 files changed

+101
-3
lines changed

4 files changed

+101
-3
lines changed

injected/integration-test/test-pages/api-manipulation/config/apis.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@
8282
"getterValue": {
8383
"type": "undefined"
8484
}
85+
},
86+
"window.definedByConfig": {
87+
"type": "descriptor",
88+
"getterValue": {
89+
"type": "string",
90+
"value": "defined!"
91+
},
92+
"define": true
93+
},
94+
"window.notDefinedByConfig": {
95+
"type": "descriptor",
96+
"getterValue": {
97+
"type": "string",
98+
"value": "should not exist"
99+
}
85100
}
86101
}
87102
}

injected/integration-test/test-pages/api-manipulation/pages/apis.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@
8989
return result;
9090
});
9191

92+
test('Define property with define: true', async () => {
93+
return [
94+
{
95+
name: "Property defined by config (define: true)",
96+
result: window.definedByConfig,
97+
expected: "defined!"
98+
},
99+
{
100+
name: "Property not defined by config (define not set)",
101+
result: window.notDefinedByConfig,
102+
expected: undefined
103+
}
104+
];
105+
});
106+
92107
// eslint-disable-next-line no-undef
93108
renderResults();
94109
</script>

injected/src/features/api-manipulation.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
*
55
* @module API manipulation
66
*/
7-
import ContentFeature from '../content-feature';
7+
import ContentFeature from '../content-feature.js';
88
// eslint-disable-next-line no-redeclare
9-
import { hasOwnProperty } from '../captured-globals';
10-
import { processAttr } from '../utils';
9+
import { hasOwnProperty } from '../captured-globals.js';
10+
import { processAttr } from '../utils.js';
1111

1212
/**
1313
* @internal
@@ -51,6 +51,9 @@ export default class ApiManipulation extends ContentFeature {
5151
if (change.configurable && typeof change.configurable !== 'boolean') {
5252
return false;
5353
}
54+
if ('define' in change && typeof change.define !== 'boolean') {
55+
return false;
56+
}
5457
return typeof change.getterValue !== 'undefined';
5558
}
5659
return false;
@@ -65,6 +68,7 @@ export default class ApiManipulation extends ContentFeature {
6568
* @property {import('../utils.js').ConfigSetting} [getterValue] - The value returned from a getter.
6669
* @property {boolean} [enumerable] - Whether the property is enumerable.
6770
* @property {boolean} [configurable] - Whether the property is configurable.
71+
* @property {boolean} [define] - Whether to define the property if it does not exist.
6872
*/
6973

7074
/**
@@ -117,6 +121,17 @@ export default class ApiManipulation extends ContentFeature {
117121
if ('configurable' in change) {
118122
descriptor.configurable = change.configurable;
119123
}
124+
// If 'define' is true and property does not exist, define it directly
125+
if (change.define === true && !(key in api)) {
126+
// Ensure descriptor has required boolean fields
127+
const defineDescriptor = {
128+
...descriptor,
129+
enumerable: typeof descriptor.enumerable !== 'boolean' ? true : descriptor.enumerable,
130+
configurable: typeof descriptor.configurable !== 'boolean' ? true : descriptor.configurable,
131+
};
132+
this.defineProperty(api, key, defineDescriptor);
133+
return;
134+
}
120135
this.wrapProperty(api, key, descriptor);
121136
}
122137
}

injected/unit-test/features.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
44
import { readFile } from 'fs/promises';
55
import * as glob from 'glob';
66
import { formatErrors } from '@duckduckgo/privacy-configuration/tests/schema-validation.js';
7+
import ApiManipulation from '../src/features/api-manipulation.js';
78

89
// TODO: Ignore eslint redeclare as we're linting for esm and cjs
910
// eslint-disable-next-line no-redeclare
@@ -114,3 +115,55 @@ describe('test-pages/*/config/*.json schema validation', () => {
114115
});
115116
}
116117
});
118+
119+
describe('ApiManipulation', () => {
120+
let apiManipulation;
121+
let dummyTarget;
122+
123+
beforeEach(() => {
124+
apiManipulation = new ApiManipulation(
125+
'apiManipulation',
126+
{},
127+
{
128+
bundledConfig: { features: { apiManipulation: { state: 'enabled', exceptions: [] } } },
129+
site: { domain: 'test.com' },
130+
platform: { version: '1.0.0' },
131+
},
132+
);
133+
dummyTarget = {};
134+
});
135+
136+
it('defines a new property if define: true is set and property does not exist', () => {
137+
const change = {
138+
type: 'descriptor',
139+
getterValue: { type: 'string', value: 'defined!' },
140+
define: true,
141+
};
142+
apiManipulation.wrapApiDescriptor(dummyTarget, 'definedByConfig', change);
143+
expect(dummyTarget.definedByConfig).toBe('defined!');
144+
});
145+
146+
it('does not define a property if define is not set and property does not exist', () => {
147+
const change = {
148+
type: 'descriptor',
149+
getterValue: { type: 'string', value: 'should not exist' },
150+
};
151+
apiManipulation.wrapApiDescriptor(dummyTarget, 'notDefinedByConfig', change);
152+
expect(dummyTarget.notDefinedByConfig).toBeUndefined();
153+
});
154+
155+
it('wraps an existing property if present', () => {
156+
Object.defineProperty(dummyTarget, 'hardwareConcurrency', {
157+
get: () => 4,
158+
configurable: true,
159+
enumerable: true,
160+
});
161+
const change = {
162+
type: 'descriptor',
163+
getterValue: { type: 'number', value: 222 },
164+
};
165+
apiManipulation.wrapApiDescriptor(dummyTarget, 'hardwareConcurrency', change);
166+
// The getter should now return 222
167+
expect(dummyTarget.hardwareConcurrency).toBe(222);
168+
});
169+
});

0 commit comments

Comments
 (0)