commit 7c0dde347f042d1320875472ecac5ce5eed93bbb
parent 1822997ff30f03812397c1106ef2b67a07d382bf
Author: Meg Viar <lmegviar@gmail.com>
Date: Fri, 24 Oct 2025 10:46:36 +0000
Bug 1981255 - Gate rendering first about:welcome screen until after screen filtering finishes r=emcminn,omc-reviewers,mimi,jprickett
Differential Revision: https://phabricator.services.mozilla.com/D269791
Diffstat:
4 files changed, 78 insertions(+), 4 deletions(-)
diff --git a/browser/components/aboutwelcome/content-src/aboutwelcome.jsx b/browser/components/aboutwelcome/content-src/aboutwelcome.jsx
@@ -74,6 +74,7 @@ class AboutWelcome extends React.PureComponent {
startScreen={props.startScreen || 0}
appAndSystemLocaleInfo={props.appAndSystemLocaleInfo}
ariaRole={props.aria_role}
+ gateInitialPaint={true}
/>
);
}
diff --git a/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx b/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx
@@ -15,6 +15,7 @@ const TRANSITION_OUT_TIME = 1000;
const LANGUAGE_MISMATCH_SCREEN_ID = "AW_LANGUAGE_MISMATCH";
export const MultiStageAboutWelcome = props => {
+ const gateInitialPaint = props.gateInitialPaint ?? false;
let { defaultScreens } = props;
const didFilter = useRef(false);
const [didMount, setDidMount] = useState(false);
@@ -22,6 +23,8 @@ export const MultiStageAboutWelcome = props => {
const [index, setScreenIndex] = useState(props.startScreen);
const [previousOrder, setPreviousOrder] = useState(props.startScreen - 1);
+ // Gate first paint until we've finished the initial filtering pass.
+ const [ready, setReady] = useState(false);
useEffect(() => {
(async () => {
@@ -56,8 +59,11 @@ export const MultiStageAboutWelcome = props => {
filtered => screens.find(s => s.id === filtered.id) ?? filtered
)
);
-
- didFilter.current = true;
+ // Mark the initial filter pass complete and allow the first paint.
+ if (!didFilter.current) {
+ didFilter.current = true;
+ setReady(true);
+ }
// After completing screen filtering, trigger any unhandled campaign
// action present in the attribution campaign data. This updates the
@@ -241,6 +247,12 @@ export const MultiStageAboutWelcome = props => {
})();
}, [index]);
+ // Do not render anything until the first filtering pass completes if gating
+ // initial paint is enabled.
+ if (gateInitialPaint && !ready) {
+ return null;
+ }
+
return (
<React.Fragment>
<div
diff --git a/browser/components/aboutwelcome/content/aboutwelcome.bundle.js b/browser/components/aboutwelcome/content/aboutwelcome.bundle.js
@@ -168,6 +168,7 @@ __webpack_require__.r(__webpack_exports__);
const TRANSITION_OUT_TIME = 1000;
const LANGUAGE_MISMATCH_SCREEN_ID = "AW_LANGUAGE_MISMATCH";
const MultiStageAboutWelcome = props => {
+ const gateInitialPaint = props.gateInitialPaint ?? false;
let {
defaultScreens
} = props;
@@ -176,6 +177,8 @@ const MultiStageAboutWelcome = props => {
const [screens, setScreens] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(defaultScreens);
const [index, setScreenIndex] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(props.startScreen);
const [previousOrder, setPreviousOrder] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(props.startScreen - 1);
+ // Gate first paint until we've finished the initial filtering pass.
+ const [ready, setReady] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
(async () => {
// If we want to load index from history state, we don't want to send impression yet
@@ -194,7 +197,11 @@ const MultiStageAboutWelcome = props => {
// Use existing screen for the filtered screen to carry over any modification
// e.g. if AW_LANGUAGE_MISMATCH exists, use it from existing screens
setScreens(filteredScreens.map(filtered => screens.find(s => s.id === filtered.id) ?? filtered));
- didFilter.current = true;
+ // Mark the initial filter pass complete and allow the first paint.
+ if (!didFilter.current) {
+ didFilter.current = true;
+ setReady(true);
+ }
// After completing screen filtering, trigger any unhandled campaign
// action present in the attribution campaign data. This updates the
@@ -361,6 +368,12 @@ const MultiStageAboutWelcome = props => {
setInstalledAddons(addons);
})();
}, [index]);
+
+ // Do not render anything until the first filtering pass completes if gating
+ // initial paint is enabled.
+ if (gateInitialPaint && !ready) {
+ return null;
+ }
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: `outer-wrapper onboardingContainer proton transition-${transition}`,
style: props.backdrop ? {
@@ -4031,7 +4044,8 @@ class AboutWelcome extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
backdrop: props.backdrop,
startScreen: props.startScreen || 0,
appAndSystemLocaleInfo: props.appAndSystemLocaleInfo,
- ariaRole: props.aria_role
+ ariaRole: props.aria_role,
+ gateInitialPaint: true
});
}
}
diff --git a/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx b/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx
@@ -218,6 +218,53 @@ describe("MultiStageAboutWelcome module", () => {
assert.equal(stub.lastCall.args[2], "SELECT_CHECKBOX");
stub.restore();
});
+
+ it("does not render anything until targeting/filtering resolves (gated first paint)", async () => {
+ let resolveTargeting;
+ const targetingPromise = new Promise(r => (resolveTargeting = r));
+ globals.set("AWEvaluateScreenTargeting", () => targetingPromise);
+
+ const wrapper = mount(
+ <MultiStageAboutWelcome {...DEFAULT_PROPS} gateInitialPaint={true} />
+ );
+ // Immediately after mount (before resolve), nothing should be painted
+ assert.strictEqual(wrapper.html(), null);
+ assert.ok(!wrapper.find(WelcomeScreen).exists());
+
+ resolveTargeting();
+ await spinEventLoop();
+ wrapper.update();
+
+ // After targeting resolves, the first screen should render
+ assert.ok(wrapper.find(WelcomeScreen).exists());
+ });
+
+ it("does not send telemetry before first paint is ready, but does afterwards", async () => {
+ let resolveTargeting;
+ const targetingPromise = new Promise(r => (resolveTargeting = r));
+ globals.set("AWEvaluateScreenTargeting", () => targetingPromise);
+
+ const sendEventStub = sinon.stub(global, "AWSendEventTelemetry");
+ const wrapper = mount(
+ <MultiStageAboutWelcome {...DEFAULT_PROPS} gateInitialPaint={true} />
+ );
+ assert.notCalled(sendEventStub);
+ assert.strictEqual(wrapper.html(), null);
+
+ resolveTargeting();
+ await spinEventLoop();
+ wrapper.update();
+
+ const hadImpression = sendEventStub
+ .getCalls()
+ .some(c => c.args && c.args[0] && c.args[0].event === "IMPRESSION");
+ assert.ok(
+ hadImpression,
+ "Expected at least one IMPRESSION event after paint"
+ );
+
+ sendEventStub.restore();
+ });
});
describe("WelcomeScreen component", () => {