page_params: Parse page_params and state_data with Zod.

This establishes a runtime check that their types continue to reflect
reality going forward.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2024-02-16 13:56:36 -08:00 committed by Tim Abbott
parent 4f1659fe8f
commit a4938d3760
25 changed files with 312 additions and 224 deletions

View File

@ -80,7 +80,9 @@ def render_stats(
translation.get_language_from_path(request.path_info),
)
# Sync this with stats_params_schema in base_page_params.ts.
page_params = dict(
page_type="stats",
data_url_suffix=data_url_suffix,
upload_space_used=space_used,
guest_users=guest_users,

View File

@ -8,7 +8,18 @@ from datetime import datetime, timedelta, timezone
from decimal import Decimal
from enum import Enum
from functools import wraps
from typing import Any, Callable, Dict, Generator, Optional, Tuple, TypedDict, TypeVar, Union
from typing import (
Any,
Callable,
Dict,
Generator,
Literal,
Optional,
Tuple,
TypedDict,
TypeVar,
Union,
)
from urllib.parse import urlencode, urljoin
import stripe
@ -615,7 +626,9 @@ class BillingSessionAuditLogEventError(Exception):
super().__init__(self.message)
# Sync this with upgrade_params_schema in base_page_params.ts.
class UpgradePageParams(TypedDict):
page_type: Literal["upgrade"]
annual_price: int
demo_organization_scheduled_deletion_date: Optional[datetime]
monthly_price: int
@ -2395,6 +2408,7 @@ class BillingSession(ABC):
"remote_server_legacy_plan_end_date": remote_server_legacy_plan_end_date,
"manual_license_management": initial_upgrade_request.manual_license_management,
"page_params": {
"page_type": "upgrade",
"annual_price": get_price_per_license(
tier, CustomerPlan.BILLING_SCHEDULE_ANNUAL, percent_off
),

View File

@ -278,7 +278,9 @@ def team_view(request: HttpRequest) -> HttpResponse:
request,
"corporate/team.html",
context={
# Sync this with team_params_schema in base_page_params.ts.
"page_params": {
"page_type": "team",
"contributors": data["contributors"],
},
"date": data["date"],

View File

@ -56,6 +56,7 @@ EXEMPT_FILES = make_set(
"web/src/attachments_ui.ts",
"web/src/audible_notifications.ts",
"web/src/avatar.ts",
"web/src/base_page_params.ts",
"web/src/billing/event_status.ts",
"web/src/billing/helpers.ts",
"web/src/blueslip.ts",

101
web/src/base_page_params.ts Normal file
View File

@ -0,0 +1,101 @@
import $ from "jquery";
import {z} from "zod";
import {state_data_schema, term_schema} from "./state_data";
const t1 = performance.now();
// Sync this with zerver.context_processors.zulip_default_context.
const default_params_schema = z.object({
page_type: z.literal("default"),
development_environment: z.boolean(),
realm_sentry_key: z.optional(z.string()),
request_language: z.string(),
server_sentry_dsn: z.nullable(z.string()),
server_sentry_environment: z.optional(z.string()),
server_sentry_sample_rate: z.optional(z.number()),
server_sentry_trace_rate: z.optional(z.number()),
});
// These parameters are sent in #page-params for both users and spectators.
//
// Sync this with zerver.lib.home.build_page_params_for_home_page_load.
const home_params_schema = default_params_schema
.extend({
page_type: z.literal("home"),
apps_page_url: z.string(),
bot_types: z.array(
z.object({
type_id: z.number(),
name: z.string(),
allowed: z.boolean(),
}),
),
corporate_enabled: z.boolean(),
furthest_read_time: z.nullable(z.number()),
is_spectator: z.boolean(),
language_list: z.array(
z.object({
code: z.string(),
locale: z.string(),
name: z.string(),
percent_translated: z.optional(z.number()),
}),
),
login_page: z.string(),
narrow: z.optional(z.array(term_schema)),
narrow_stream: z.optional(z.string()),
needs_tutorial: z.boolean(),
promote_sponsoring_zulip: z.boolean(),
show_billing: z.boolean(),
show_plans: z.boolean(),
show_webathena: z.boolean(),
sponsorship_pending: z.boolean(),
state_data: state_data_schema.optional(),
translation_data: z.record(z.string()),
})
// TODO/typescript: Remove .passthrough() when all consumers have been
// converted to TypeScript and the schema is complete.
.passthrough();
// Sync this with analytics.views.stats.render_stats.
const stats_params_schema = default_params_schema.extend({
page_type: z.literal("stats"),
data_url_suffix: z.string(),
upload_space_used: z.nullable(z.number()),
guest_users: z.nullable(z.number()),
translation_data: z.record(z.string()),
});
// Sync this with corporate.views.portico.team_view.
const team_params_schema = default_params_schema.extend({
page_type: z.literal("team"),
contributors: z.unknown(),
});
// Sync this with corporate.lib.stripe.UpgradePageParams.
const upgrade_params_schema = default_params_schema.extend({
page_type: z.literal("upgrade"),
annual_price: z.number(),
demo_organization_scheduled_deletion_date: z.nullable(z.number()),
monthly_price: z.number(),
seat_count: z.number(),
billing_base_url: z.string(),
tier: z.number(),
flat_discount: z.number(),
flat_discounted_months: z.number(),
fixed_price: z.number().nullable(),
});
const page_params_schema = z.discriminatedUnion("page_type", [
default_params_schema,
home_params_schema,
stats_params_schema,
team_params_schema,
upgrade_params_schema,
]);
export const page_params = page_params_schema.parse($("#page-params").remove().data("params"));
const t2 = performance.now();
export const page_params_parse_time = t2 - t1;

View File

@ -1,19 +1,9 @@
import $ from "jquery";
import assert from "minimalistic-assert";
// Don't remove page_params here yet, since we still use them later.
// For example, "#page_params" is used again through `sentry.ts`, which
// imports the main `src/page_params` module.
export const page_params: {
annual_price: number;
monthly_price: number;
seat_count: number;
billing_base_url: string;
tier: number;
flat_discount: number;
flat_discounted_months: number;
fixed_price: number | null;
} = $("#page-params").data("params");
import {page_params as base_page_params} from "../base_page_params";
if (!page_params) {
throw new Error("Missing page-params");
}
assert(base_page_params.page_type === "upgrade");
// We need to export with a narrowed TypeScript type
// eslint-disable-next-line unicorn/prefer-export-from
export const page_params = base_page_params;

View File

@ -9,8 +9,8 @@
import * as Sentry from "@sentry/browser";
import $ from "jquery";
import {page_params} from "./base_page_params";
import {BlueslipError, display_stacktrace} from "./blueslip_stacktrace";
import {page_params} from "./page_params";
if (Error.stackTraceLimit !== undefined) {
Error.stackTraceLimit = 100000;

View File

@ -12,6 +12,7 @@ import type {Message} from "./message_store";
import {page_params} from "./page_params";
import * as people from "./people";
import {realm} from "./state_data";
import type {Term} from "./state_data";
import * as stream_data from "./stream_data";
import type {StreamSubscription} from "./sub_store";
import * as unread from "./unread";
@ -234,12 +235,6 @@ function message_matches_search_term(message: Message, operator: string, operand
return true; // unknown operators return true (effectively ignored)
}
export type Term = {
negated?: boolean;
operator: string;
operand: string;
};
export class Filter {
_terms: Term[];
_sub?: StreamSubscription;

View File

@ -1,5 +1,5 @@
import {realm} from "./state_data";
import type {GroupPermissionSetting} from "./types";
import type {GroupPermissionSetting} from "./state_data";
export function get_group_permission_setting_config(
setting_name: string,

View File

@ -6,14 +6,14 @@ import {DEFAULT_INTL_CONFIG, IntlErrorCode, createIntl, createIntlCache} from "@
import type {FormatXMLElementFn, PrimitiveType} from "intl-messageformat";
import _ from "lodash";
import {page_params} from "./page_params";
import {page_params} from "./base_page_params";
const cache = createIntlCache();
export const intl = createIntl(
{
locale: page_params.request_language,
defaultLocale: "en",
messages: page_params.translation_data,
messages: "translation_data" in page_params ? page_params.translation_data : {},
/* istanbul ignore next */
onError(error) {
// Ignore complaints about untranslated strings that were
@ -50,7 +50,7 @@ export function $t_html(
});
}
export let language_list: (typeof page_params)["language_list"];
export let language_list: (typeof page_params & {page_type: "home"})["language_list"];
export function get_language_name(language_code: string): string {
const language_list_map: Record<string, string> = {};

View File

@ -1,10 +1,10 @@
import * as blueslip from "./blueslip";
import {Filter} from "./filter";
import type {Term} from "./filter";
import * as inbox_util from "./inbox_util";
import {page_params} from "./page_params";
import * as people from "./people";
import * as recent_view_util from "./recent_view_util";
import type {Term} from "./state_data";
import * as stream_data from "./stream_data";
import type {StreamSubscription} from "./sub_store";
import * as unread from "./unread";

View File

@ -1,44 +1,9 @@
import $ from "jquery";
import assert from "minimalistic-assert";
import type {Term} from "./filter";
import {page_params as base_page_params} from "./base_page_params";
const t1 = performance.now();
export const page_params: {
apps_page_url: string;
bot_types: {
type_id: number;
name: string;
allowed: boolean;
}[];
corporate_enabled: boolean;
development_environment: boolean;
furthest_read_time: number | null;
is_spectator: boolean;
language_list: {
code: string;
locale: string;
name: string;
percent_translated?: number;
}[];
login_page: string;
narrow?: Term[];
narrow_stream?: string;
needs_tutorial: boolean;
promote_sponsoring_zulip: boolean;
realm_sentry_key?: string;
request_language: string;
server_sentry_dsn: string | null;
server_sentry_environment?: string;
server_sentry_sample_rate?: number;
server_sentry_trace_rate?: number;
show_billing: boolean;
show_plans: boolean;
show_webathena: boolean;
sponsorship_pending: boolean;
translation_data: Record<string, string>;
} = $("#page-params").remove().data("params");
const t2 = performance.now();
export const page_params_parse_time = t2 - t1;
if (!page_params) {
throw new Error("Missing page-params");
}
assert(base_page_params.page_type === "home");
// We need to export with a narrowed TypeScript type.
// eslint-disable-next-line unicorn/prefer-export-from
export const page_params = base_page_params;

View File

@ -1,6 +1,6 @@
import {gtag, install} from "ga-gtag";
import {page_params} from "../page_params";
import {page_params} from "../base_page_params";
export let config;

View File

@ -1,6 +1,7 @@
import $ from "jquery";
import assert from "minimalistic-assert";
import {page_params} from "../page_params";
import {page_params} from "../base_page_params";
import {detect_user_os} from "./tabbed-instructions";
import render_tabs from "./team";
@ -119,6 +120,7 @@ $(() => {
events();
if (window.location.pathname === "/team/") {
assert(page_params.page_type === "team");
const contributors = page_params.contributors;
delete page_params.contributors;
render_tabs(contributors);

View File

@ -1,6 +1,6 @@
import * as Sentry from "@sentry/browser";
import {page_params} from "./page_params";
import {page_params} from "./base_page_params";
import {current_user, realm} from "./state_data";
type UserInfo = {
@ -76,7 +76,11 @@ if (page_params.server_sentry_dsn) {
const user_info: UserInfo = {
realm: sentry_key,
};
if (sentry_key !== "www" && current_user !== undefined) {
if (
sentry_key !== "www" &&
page_params.page_type === "home" &&
current_user !== undefined
) {
user_info.role = current_user.is_owner
? "Organization owner"
: current_user.is_admin

View File

@ -1,7 +1,7 @@
import Handlebars from "handlebars/runtime";
import {page_params} from "./base_page_params";
import {$t, $t_html} from "./i18n";
import {page_params} from "./page_params";
import type {RealmDefaultSettings} from "./realm_user_settings_defaults";
import {realm} from "./state_data";
import type {StreamSpecificNotificationSettings} from "./sub_store";

View File

@ -1,131 +1,145 @@
import type {GroupPermissionSetting} from "./types";
import {z} from "zod";
export let current_user: {
avatar_source: string;
delivery_email: string;
is_admin: boolean;
is_billing_admin: boolean;
is_guest: boolean;
is_moderator: boolean;
is_owner: boolean;
user_id: number;
};
const group_permission_setting_schema = z.object({
require_system_group: z.boolean(),
allow_internet_group: z.boolean(),
allow_owners_group: z.boolean(),
allow_nobody_group: z.boolean(),
allow_everyone_group: z.boolean(),
default_group_name: z.string(),
id_field_name: z.string(),
default_for_system_groups: z.nullable(z.string()),
allowed_system_groups: z.array(z.string()),
});
export type GroupPermissionSetting = z.output<typeof group_permission_setting_schema>;
export let realm: {
custom_profile_fields: {
display_in_profile_summary?: boolean;
field_data: string;
hint: string;
id: number;
name: string;
order: number;
type: number;
}[];
custom_profile_field_types: {
SHORT_TEXT: {
id: number;
name: string;
};
LONG_TEXT: {
id: number;
name: string;
};
DATE: {
id: number;
name: string;
};
SELECT: {
id: number;
name: string;
};
URL: {
id: number;
name: string;
};
EXTERNAL_ACCOUNT: {
id: number;
name: string;
};
USER: {
id: number;
name: string;
};
PRONOUNS: {
id: number;
name: string;
};
};
max_avatar_file_size_mib: number;
max_icon_file_size_mib: number;
max_logo_file_size_mib: number;
realm_add_custom_emoji_policy: number;
realm_available_video_chat_providers: {
disabled: {name: string; id: number};
jitsi_meet: {name: string; id: number};
zoom?: {name: string; id: number};
big_blue_button?: {name: string; id: number};
};
realm_avatar_changes_disabled: boolean;
realm_bot_domain: string;
realm_can_access_all_users_group: number;
realm_create_multiuse_invite_group: number;
realm_create_private_stream_policy: number;
realm_create_public_stream_policy: number;
realm_create_web_public_stream_policy: number;
realm_delete_own_message_policy: number;
realm_description: string;
realm_domains: {domain: string; allow_subdomains: boolean}[];
realm_edit_topic_policy: number;
realm_email_changes_disabled: boolean;
realm_enable_guest_user_indicator: boolean;
realm_enable_spectator_access: boolean;
realm_icon_source: string;
realm_icon_url: string;
realm_invite_to_realm_policy: number;
realm_invite_to_stream_policy: number;
realm_is_zephyr_mirror_realm: boolean;
realm_jitsi_server_url: string | null;
realm_logo_source: string;
realm_logo_url: string;
realm_move_messages_between_streams_policy: number;
realm_name_changes_disabled: boolean;
realm_name: string;
realm_night_logo_source: string;
realm_night_logo_url: string;
realm_notifications_stream_id: number;
realm_org_type: number;
realm_plan_type: number;
realm_private_message_policy: number;
realm_push_notifications_enabled: boolean;
realm_upload_quota_mib: number | null;
realm_uri: string;
realm_user_group_edit_policy: number;
realm_video_chat_provider: number;
realm_waiting_period_threshold: number;
server_avatar_changes_disabled: boolean;
server_jitsi_server_url: string | null;
server_name_changes_disabled: boolean;
server_needs_upgrade: boolean;
server_presence_offline_threshold_seconds: number;
server_supported_permission_settings: {
realm: Record<string, GroupPermissionSetting>;
stream: Record<string, GroupPermissionSetting>;
group: Record<string, GroupPermissionSetting>;
};
server_typing_started_expiry_period_milliseconds: number;
server_typing_started_wait_period_milliseconds: number;
server_typing_stopped_wait_period_milliseconds: number;
server_web_public_streams_enabled: boolean;
stop_words: string[];
zulip_merge_base: string;
zulip_plan_is_not_limited: boolean;
zulip_version: string;
};
export const term_schema = z.object({
negated: z.optional(z.boolean()),
operator: z.string(),
operand: z.string(),
});
export type Term = z.output<typeof term_schema>;
// Sync this with zerver.lib.events.do_events_register.
export function set_current_user(initial_current_user: typeof current_user): void {
export const current_user_schema = z.object({
avatar_source: z.string(),
delivery_email: z.string(),
is_admin: z.boolean(),
is_billing_admin: z.boolean(),
is_guest: z.boolean(),
is_moderator: z.boolean(),
is_owner: z.boolean(),
user_id: z.number(),
});
// Sync this with zerver.lib.events.do_events_register.
export const realm_schema = z.object({
custom_profile_fields: z.array(
z.object({
display_in_profile_summary: z.optional(z.boolean()),
field_data: z.string(),
hint: z.string(),
id: z.number(),
name: z.string(),
order: z.number(),
type: z.number(),
}),
),
custom_profile_field_types: z.object({
SHORT_TEXT: z.object({id: z.number(), name: z.string()}),
LONG_TEXT: z.object({id: z.number(), name: z.string()}),
DATE: z.object({id: z.number(), name: z.string()}),
SELECT: z.object({id: z.number(), name: z.string()}),
URL: z.object({id: z.number(), name: z.string()}),
EXTERNAL_ACCOUNT: z.object({id: z.number(), name: z.string()}),
USER: z.object({id: z.number(), name: z.string()}),
PRONOUNS: z.object({id: z.number(), name: z.string()}),
}),
max_avatar_file_size_mib: z.number(),
max_icon_file_size_mib: z.number(),
max_logo_file_size_mib: z.number(),
realm_add_custom_emoji_policy: z.number(),
realm_available_video_chat_providers: z.object({
disabled: z.object({name: z.string(), id: z.number()}),
jitsi_meet: z.object({name: z.string(), id: z.number()}),
zoom: z.optional(z.object({name: z.string(), id: z.number()})),
big_blue_button: z.optional(z.object({name: z.string(), id: z.number()})),
}),
realm_avatar_changes_disabled: z.boolean(),
realm_bot_domain: z.string(),
realm_can_access_all_users_group: z.number(),
realm_create_multiuse_invite_group: z.number(),
realm_create_private_stream_policy: z.number(),
realm_create_public_stream_policy: z.number(),
realm_create_web_public_stream_policy: z.number(),
realm_delete_own_message_policy: z.number(),
realm_description: z.string(),
realm_domains: z.array(
z.object({
domain: z.string(),
allow_subdomains: z.boolean(),
}),
),
realm_edit_topic_policy: z.number(),
realm_email_changes_disabled: z.boolean(),
realm_enable_guest_user_indicator: z.boolean(),
realm_enable_spectator_access: z.boolean(),
realm_icon_source: z.string(),
realm_icon_url: z.string(),
realm_invite_to_realm_policy: z.number(),
realm_invite_to_stream_policy: z.number(),
realm_is_zephyr_mirror_realm: z.boolean(),
realm_jitsi_server_url: z.nullable(z.string()),
realm_logo_source: z.string(),
realm_logo_url: z.string(),
realm_move_messages_between_streams_policy: z.number(),
realm_name_changes_disabled: z.boolean(),
realm_name: z.string(),
realm_night_logo_source: z.string(),
realm_night_logo_url: z.string(),
realm_notifications_stream_id: z.number(),
realm_org_type: z.number(),
realm_plan_type: z.number(),
realm_private_message_policy: z.number(),
realm_push_notifications_enabled: z.boolean(),
realm_upload_quota_mib: z.nullable(z.number()),
realm_uri: z.string(),
realm_user_group_edit_policy: z.number(),
realm_video_chat_provider: z.number(),
realm_waiting_period_threshold: z.number(),
server_avatar_changes_disabled: z.boolean(),
server_jitsi_server_url: z.nullable(z.string()),
server_name_changes_disabled: z.boolean(),
server_needs_upgrade: z.boolean(),
server_presence_offline_threshold_seconds: z.number(),
server_supported_permission_settings: z.object({
realm: z.record(group_permission_setting_schema),
stream: z.record(group_permission_setting_schema),
group: z.record(group_permission_setting_schema),
}),
server_typing_started_expiry_period_milliseconds: z.number(),
server_typing_started_wait_period_milliseconds: z.number(),
server_typing_stopped_wait_period_milliseconds: z.number(),
server_web_public_streams_enabled: z.boolean(),
stop_words: z.array(z.string()),
zulip_merge_base: z.string(),
zulip_plan_is_not_limited: z.boolean(),
zulip_version: z.string(),
});
export const state_data_schema = current_user_schema
.merge(realm_schema)
// TODO/typescript: Remove .passthrough() when all consumers have been
// converted to TypeScript and the schema is complete.
.passthrough();
export let current_user: z.infer<typeof current_user_schema>;
export let realm: z.infer<typeof realm_schema>;
export function set_current_user(initial_current_user: z.infer<typeof current_user_schema>): void {
current_user = initial_current_user;
}
export function set_realm(initial_realm: typeof realm): void {
export function set_realm(initial_realm: z.infer<typeof realm_schema>): void {
realm = initial_realm;
}

View File

@ -1,11 +1,9 @@
import $ from "jquery";
import assert from "minimalistic-assert";
export const page_params: {
data_url_suffix: string;
guest_users: number | null;
upload_space_used: number | null;
} = $("#page-params").data("params");
import {page_params as base_page_params} from "../base_page_params";
if (!page_params) {
throw new Error("Missing page-params");
}
assert(base_page_params.page_type === "stats");
// We need to export with a narrowed TypeScript type
// eslint-disable-next-line unicorn/prefer-export-from
export const page_params = base_page_params;

View File

@ -52,15 +52,3 @@ export type UpdateMessageEvent = {
// This will not be set until it gets fixed.
topic?: string;
};
export type GroupPermissionSetting = {
require_system_group: boolean;
allow_internet_group: boolean;
allow_owners_group: boolean;
allow_nobody_group: boolean;
allow_everyone_group: boolean;
default_group_name: string;
id_field_name: string;
default_for_system_groups: string | null;
allowed_system_groups: string[];
};

View File

@ -1,5 +1,6 @@
import $ from "jquery";
import _ from "lodash";
import assert from "minimalistic-assert";
import generated_emoji_codes from "../../static/generated/emoji/emoji_codes.json";
import * as fenced_code from "../shared/src/fenced_code";
@ -114,7 +115,7 @@ import * as sidebar_ui from "./sidebar_ui";
import * as spoilers from "./spoilers";
import * as starred_messages from "./starred_messages";
import * as starred_messages_ui from "./starred_messages_ui";
import {current_user, realm, set_current_user, set_realm} from "./state_data";
import {current_user, realm, set_current_user, set_realm, state_data_schema} from "./state_data";
import * as stream_data from "./stream_data";
import * as stream_edit from "./stream_edit";
import * as stream_edit_subscribers from "./stream_edit_subscribers";
@ -874,7 +875,8 @@ $(async () => {
url: "/json/register",
data,
success(response_data) {
initialize_everything(response_data);
const state_data = state_data_schema.parse(response_data);
initialize_everything(state_data);
},
error() {
$("#app-loading-middle-content").hide();
@ -884,6 +886,7 @@ $(async () => {
},
});
} else {
assert(page_params.state_data !== undefined);
initialize_everything(page_params.state_data);
}
});

View File

@ -9,7 +9,7 @@ export {get_stream_id, get_sub, get_subscriber_count} from "./stream_data";
export {get_by_user_id as get_person_by_user_id, get_user_id_from_name} from "./people";
export {last_visible as last_visible_row, id as row_id} from "./rows";
export {cancel as cancel_compose} from "./compose_actions";
export {page_params, page_params_parse_time} from "./page_params";
export {page_params, page_params_parse_time} from "./base_page_params";
export {initiate as initiate_reload} from "./reload";
export {page_load_time} from "./setup";
export {current_user, realm} from "./state_data";

View File

@ -118,6 +118,8 @@ test.set_verbose(files.length === 1);
require("../../src/blueslip");
namespace.mock_esm("../../src/i18n", stub_i18n);
require("../../src/i18n");
namespace.mock_esm("../../src/base_page_params", zpage_params);
require("../../src/base_page_params");
namespace.mock_esm("../../src/billing/page_params", zpage_billing_params);
require("../../src/billing/page_params");
namespace.mock_esm("../../src/page_params", zpage_params);

View File

@ -32,6 +32,7 @@ from zproject.backends import (
from zproject.config import get_config
DEFAULT_PAGE_PARAMS: Mapping[str, Any] = {
"page_type": "default",
"development_environment": settings.DEVELOPMENT,
}
@ -165,6 +166,7 @@ def zulip_default_context(request: HttpRequest) -> Dict[str, Any]:
f'<a href="mailto:{escape(support_email)}">{escape(support_email)}</a>'
)
# Sync this with default_params_schema in base_page_params.ts.
default_page_params: Dict[str, Any] = {
**DEFAULT_PAGE_PARAMS,
"server_sentry_dsn": settings.SENTRY_FRONTEND_DSN,

View File

@ -195,7 +195,10 @@ def build_page_params_for_home_page_load(
# Pass parameters to the client-side JavaScript code.
# These end up in a JavaScript Object named 'page_params'.
#
# Sync this with home_params_schema in base_page_params.ts.
page_params: Dict[str, object] = dict(
page_type="home",
## Server settings.
test_suite=settings.TEST_SUITE,
insecure_desktop_app=insecure_desktop_app,

View File

@ -52,6 +52,7 @@ class HomeTest(ZulipTestCase):
"narrow_stream",
"needs_tutorial",
"no_event_queue",
"page_type",
"promote_sponsoring_zulip",
"request_language",
"server_sentry_dsn",
@ -347,6 +348,7 @@ class HomeTest(ZulipTestCase):
"login_page",
"needs_tutorial",
"no_event_queue",
"page_type",
"promote_sponsoring_zulip",
"realm_rendered_description",
"request_language",