327 lines
8.7 KiB
JavaScript
327 lines
8.7 KiB
JavaScript
const { ClientEnvironment } = ChromeUtils.importESModule(
|
|
"resource://normandy/lib/ClientEnvironment.sys.mjs"
|
|
);
|
|
const { TargetingContext } = ChromeUtils.importESModule(
|
|
"resource://messaging-system/targeting/Targeting.sys.mjs"
|
|
);
|
|
const { TelemetryTestUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/TelemetryTestUtils.sys.mjs"
|
|
);
|
|
const { TestUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/TestUtils.sys.mjs"
|
|
);
|
|
|
|
add_task(async function instance_with_default() {
|
|
let targeting = new TargetingContext();
|
|
|
|
let res = await targeting.eval(
|
|
`ctx.locale == '${Services.locale.appLocaleAsBCP47}'`
|
|
);
|
|
|
|
Assert.ok(res, "Has local context");
|
|
});
|
|
|
|
add_task(async function instance_with_context() {
|
|
let targeting = new TargetingContext({ bar: 42 });
|
|
|
|
let res = await targeting.eval("ctx.bar == 42");
|
|
|
|
Assert.ok(res, "Merge provided context with default");
|
|
});
|
|
|
|
add_task(async function eval_1_context() {
|
|
let targeting = new TargetingContext();
|
|
|
|
let res = await targeting.eval("custom1.bar == 42", { custom1: { bar: 42 } });
|
|
|
|
Assert.ok(res, "Eval uses provided context");
|
|
});
|
|
|
|
add_task(async function eval_2_context() {
|
|
let targeting = new TargetingContext();
|
|
|
|
let res = await targeting.eval("custom1.bar == 42 && custom2.foo == 42", {
|
|
custom1: { bar: 42 },
|
|
custom2: { foo: 42 },
|
|
});
|
|
|
|
Assert.ok(res, "Eval uses provided context");
|
|
});
|
|
|
|
add_task(async function eval_multiple_context() {
|
|
let targeting = new TargetingContext();
|
|
|
|
let res = await targeting.eval(
|
|
"custom1.bar == 42 && custom2.foo == 42 && custom3.baz == 42",
|
|
{ custom1: { bar: 42 }, custom2: { foo: 42 } },
|
|
{ custom3: { baz: 42 } }
|
|
);
|
|
|
|
Assert.ok(res, "Eval uses provided context");
|
|
});
|
|
|
|
add_task(async function eval_multiple_context_precedence() {
|
|
let targeting = new TargetingContext();
|
|
|
|
let res = await targeting.eval(
|
|
"custom1.bar == 42 && custom2.foo == 42",
|
|
{ custom1: { bar: 24 }, custom2: { foo: 24 } },
|
|
{ custom1: { bar: 42 }, custom2: { foo: 42 } }
|
|
);
|
|
|
|
Assert.ok(res, "Last provided context overrides previously defined ones.");
|
|
});
|
|
|
|
add_task(async function eval_evalWithDefault() {
|
|
let targeting = new TargetingContext({ foo: 42 });
|
|
|
|
let res = await targeting.evalWithDefault("foo == 42");
|
|
|
|
Assert.ok(res, "Eval uses provided context");
|
|
});
|
|
|
|
add_task(async function log_targeting_error_events() {
|
|
let ctx = {
|
|
get foo() {
|
|
throw new Error("unit test");
|
|
},
|
|
};
|
|
let targeting = new TargetingContext(ctx);
|
|
let stub = sinon.stub(targeting, "_sendUndesiredEvent");
|
|
|
|
await Assert.rejects(
|
|
targeting.evalWithDefault("foo == 42", ctx),
|
|
/unit test/,
|
|
"Getter should throw"
|
|
);
|
|
|
|
Assert.equal(stub.callCount, 1, "Error event was logged");
|
|
let {
|
|
args: [{ event, value }],
|
|
} = stub.firstCall;
|
|
Assert.equal(event, "AttributeError", "Correct error message");
|
|
Assert.equal(value, "foo", "Correct attribute name");
|
|
});
|
|
|
|
add_task(async function eval_evalWithDefault_precedence() {
|
|
let targeting = new TargetingContext({ region: "space" });
|
|
let res = await targeting.evalWithDefault("region != 'space'");
|
|
|
|
Assert.ok(res, "Custom context does not override TargetingEnvironment");
|
|
});
|
|
|
|
add_task(async function eval_evalWithDefault_combineContexts() {
|
|
let combinedCtxs = TargetingContext.combineContexts({ foo: 1 }, { foo: 2 });
|
|
let targeting = new TargetingContext(combinedCtxs);
|
|
let res = await targeting.evalWithDefault("foo == 1");
|
|
|
|
Assert.ok(res, "First match is returned for combineContexts");
|
|
});
|
|
|
|
add_task(async function log_targeting_error_events_in_namespace() {
|
|
let ctx = {
|
|
get foo() {
|
|
throw new Error("unit test");
|
|
},
|
|
};
|
|
let targeting = new TargetingContext(ctx);
|
|
let stub = sinon.stub(targeting, "_sendUndesiredEvent");
|
|
let catchStub = sinon.stub();
|
|
|
|
try {
|
|
await targeting.eval("ctx.foo == 42");
|
|
} catch (e) {
|
|
catchStub();
|
|
}
|
|
|
|
Assert.equal(stub.callCount, 1, "Error event was logged");
|
|
let {
|
|
args: [{ event, value }],
|
|
} = stub.firstCall;
|
|
Assert.equal(event, "AttributeError", "Correct error message");
|
|
Assert.equal(value, "ctx.foo", "Correct attribute name");
|
|
Assert.ok(catchStub.calledOnce, "eval throws errors");
|
|
});
|
|
|
|
add_task(async function log_timeout_errors() {
|
|
let ctx = {
|
|
timeout: 1,
|
|
get foo() {
|
|
return new Promise(() => {});
|
|
},
|
|
};
|
|
|
|
let targeting = new TargetingContext(ctx);
|
|
let stub = sinon.stub(targeting, "_sendUndesiredEvent");
|
|
let catchStub = sinon.stub();
|
|
|
|
try {
|
|
await targeting.eval("ctx.foo");
|
|
} catch (e) {
|
|
catchStub();
|
|
}
|
|
|
|
Assert.equal(catchStub.callCount, 1, "Timeout error throws");
|
|
Assert.equal(stub.callCount, 1, "Timeout event was logged");
|
|
let {
|
|
args: [{ event, value }],
|
|
} = stub.firstCall;
|
|
Assert.equal(event, "AttributeTimeout", "Correct error message");
|
|
Assert.equal(value, "ctx.foo", "Correct attribute name");
|
|
});
|
|
|
|
add_task(async function test_telemetry_event_timeout() {
|
|
Services.telemetry.clearEvents();
|
|
let ctx = {
|
|
timeout: 1,
|
|
get foo() {
|
|
return new Promise(() => {});
|
|
},
|
|
};
|
|
let expectedEvents = [
|
|
["messaging_experiments", "targeting", "attribute_timeout", "ctx.foo"],
|
|
];
|
|
let targeting = new TargetingContext(ctx);
|
|
|
|
try {
|
|
await targeting.eval("ctx.foo");
|
|
} catch (e) {}
|
|
|
|
TelemetryTestUtils.assertEvents(expectedEvents);
|
|
Services.telemetry.clearEvents();
|
|
});
|
|
|
|
add_task(async function test_telemetry_event_error() {
|
|
Services.telemetry.clearEvents();
|
|
let ctx = {
|
|
get bar() {
|
|
throw new Error("unit test");
|
|
},
|
|
};
|
|
let expectedEvents = [
|
|
["messaging_experiments", "targeting", "attribute_error", "ctx.bar"],
|
|
];
|
|
let targeting = new TargetingContext(ctx);
|
|
|
|
try {
|
|
await targeting.eval("ctx.bar");
|
|
} catch (e) {}
|
|
|
|
TelemetryTestUtils.assertEvents(expectedEvents);
|
|
Services.telemetry.clearEvents();
|
|
});
|
|
|
|
// Make sure that when using the Normandy-style ClientEnvironment context,
|
|
// `liveTelemetry` works. `liveTelemetry` is a particularly tricky object to
|
|
// proxy, so it's useful to check specifically.
|
|
add_task(async function test_live_telemetry() {
|
|
let ctx = { env: ClientEnvironment };
|
|
let targeting = new TargetingContext();
|
|
// This shouldn't throw.
|
|
await targeting.eval("env.liveTelemetry.main", ctx);
|
|
});
|
|
|
|
add_task(async function test_default_targeting() {
|
|
const targeting = new TargetingContext();
|
|
const expected_attributes = [
|
|
"locale",
|
|
"localeLanguageCode",
|
|
// "region", // Not available in test, requires network access to determine
|
|
"userId",
|
|
"version",
|
|
"channel",
|
|
"platform",
|
|
];
|
|
|
|
for (let attribute of expected_attributes) {
|
|
let res = await targeting.eval(`ctx.${attribute}`);
|
|
Assert.ok(res, `[eval] result for ${attribute} should not be null`);
|
|
}
|
|
|
|
for (let attribute of expected_attributes) {
|
|
let res = await targeting.evalWithDefault(attribute);
|
|
Assert.ok(
|
|
res,
|
|
`[evalWithDefault] result for ${attribute} should not be null`
|
|
);
|
|
}
|
|
});
|
|
|
|
add_task(async function test_targeting_os() {
|
|
const targeting = new TargetingContext();
|
|
await TestUtils.waitForCondition(() =>
|
|
targeting.eval("ctx.os.isWindows || ctx.os.isMac || ctx.os.isLinux")
|
|
);
|
|
let res = await targeting.eval(
|
|
`(ctx.os.isWindows && ctx.os.windowsVersion && ctx.os.windowsBuildNumber) ||
|
|
(ctx.os.isMac && ctx.os.macVersion && ctx.os.darwinVersion) ||
|
|
(ctx.os.isLinux && os.darwinVersion == null)
|
|
`
|
|
);
|
|
Assert.ok(res, `Should detect platform version got: ${res}`);
|
|
});
|
|
|
|
add_task(async function test_targeting_source_constructor() {
|
|
Services.telemetry.clearEvents();
|
|
const targeting = new TargetingContext(
|
|
{
|
|
foo: true,
|
|
get bar() {
|
|
throw new Error("bar");
|
|
},
|
|
},
|
|
{ source: "unit_testing" }
|
|
);
|
|
|
|
let res = await targeting.eval("ctx.foo");
|
|
Assert.ok(res, "Should eval to true");
|
|
|
|
let expectedEvents = [
|
|
[
|
|
"messaging_experiments",
|
|
"targeting",
|
|
"attribute_error",
|
|
"ctx.bar",
|
|
{ source: "unit_testing" },
|
|
],
|
|
];
|
|
try {
|
|
await targeting.eval("ctx.bar");
|
|
} catch (e) {}
|
|
|
|
TelemetryTestUtils.assertEvents(expectedEvents);
|
|
Services.telemetry.clearEvents();
|
|
});
|
|
|
|
add_task(async function test_targeting_source_override() {
|
|
Services.telemetry.clearEvents();
|
|
const targeting = new TargetingContext(
|
|
{
|
|
foo: true,
|
|
get bar() {
|
|
throw new Error("bar");
|
|
},
|
|
},
|
|
{ source: "unit_testing" }
|
|
);
|
|
|
|
let res = await targeting.eval("ctx.foo");
|
|
Assert.ok(res, "Should eval to true");
|
|
|
|
let expectedEvents = [
|
|
[
|
|
"messaging_experiments",
|
|
"targeting",
|
|
"attribute_error",
|
|
"bar",
|
|
{ source: "override" },
|
|
],
|
|
];
|
|
try {
|
|
targeting.setTelemetrySource("override");
|
|
await targeting.evalWithDefault("bar");
|
|
} catch (e) {}
|
|
|
|
TelemetryTestUtils.assertEvents(expectedEvents);
|
|
Services.telemetry.clearEvents();
|
|
});
|