зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1804502 - Add test cases for telemetry events for relay integration. r=credential-management-reviewers,dimi
Depends on D163911 Differential Revision: https://phabricator.services.mozilla.com/D171190
This commit is contained in:
Родитель
78b2232569
Коммит
ab4c7cefc9
|
@ -27,7 +27,7 @@ const { TelemetryUtils } = ChromeUtils.import(
|
|||
const lazy = {};
|
||||
|
||||
// Static configuration
|
||||
const config = (function() {
|
||||
const gConfig = (function() {
|
||||
const baseUrl = Services.prefs.getStringPref(
|
||||
"signon.firefoxRelay.base_url",
|
||||
undefined
|
||||
|
@ -66,7 +66,7 @@ if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
|
|||
|
||||
async function getRelayTokenAsync() {
|
||||
try {
|
||||
return await lazy.fxAccounts.getOAuthToken({ scope: config.scope });
|
||||
return await lazy.fxAccounts.getOAuthToken({ scope: gConfig.scope });
|
||||
} catch (e) {
|
||||
console.error(`There was an error getting the user's token: ${e.message}`);
|
||||
return undefined;
|
||||
|
@ -118,7 +118,7 @@ async function isRelayUserAsync() {
|
|||
|
||||
const response = await fetchWithReauth(
|
||||
null,
|
||||
headers => new Request(config.profilesUrl, { headers })
|
||||
headers => new Request(gConfig.profilesUrl, { headers })
|
||||
);
|
||||
if (!response) {
|
||||
return false;
|
||||
|
@ -139,7 +139,7 @@ async function getReusableMasksAsync(browser, _origin) {
|
|||
const response = await fetchWithReauth(
|
||||
browser,
|
||||
headers =>
|
||||
new Request(config.addressesUrl, {
|
||||
new Request(gConfig.addressesUrl, {
|
||||
method: "GET",
|
||||
headers,
|
||||
})
|
||||
|
@ -197,7 +197,7 @@ async function showErrorAsync(browser, messageId, messageArgs) {
|
|||
autofocus: true,
|
||||
removeOnDismissal: true,
|
||||
popupIconURL: "page-icon:https://relay.firefox.com",
|
||||
learnMoreURL: config.learnMoreURL,
|
||||
learnMoreURL: gConfig.learnMoreURL,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -255,7 +255,7 @@ async function showReusableMasksAsync(browser, origin, error) {
|
|||
"get_unlimited_masks",
|
||||
FirefoxRelay.flowId
|
||||
);
|
||||
browser.ownerGlobal.openWebLinkIn(config.learnMoreURL, "tab");
|
||||
browser.ownerGlobal.openWebLinkIn(gConfig.learnMoreURL, "tab");
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -357,7 +357,7 @@ async function generateUsernameAsync(browser, origin) {
|
|||
const response = await fetchWithReauth(
|
||||
browser,
|
||||
headers =>
|
||||
new Request(config.addressesUrl, {
|
||||
new Request(gConfig.addressesUrl, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
|
@ -477,11 +477,11 @@ class RelayOffered {
|
|||
async callback() {
|
||||
lazy.log.info("user opted in to Firefox Relay integration");
|
||||
feature.markAsEnabled();
|
||||
fillUsername(await generateUsernameAsync(browser, origin));
|
||||
FirefoxRelayTelemetry.recordRelayOptInPanelEvent(
|
||||
"enabled",
|
||||
FirefoxRelay.flowId
|
||||
);
|
||||
fillUsername(await generateUsernameAsync(browser, origin));
|
||||
},
|
||||
};
|
||||
const postpone = {
|
||||
|
@ -524,7 +524,7 @@ class RelayOffered {
|
|||
{
|
||||
autofocus: true,
|
||||
removeOnDismissal: true,
|
||||
learnMoreURL: config.learnMoreURL,
|
||||
learnMoreURL: gConfig.learnMoreURL,
|
||||
eventCallback: event => {
|
||||
switch (event) {
|
||||
case "shown":
|
||||
|
@ -599,12 +599,26 @@ class RelayFeature extends OptInFeature {
|
|||
static AUTH_TOKEN_ERROR_CODE = 418;
|
||||
|
||||
constructor() {
|
||||
super(RelayOffered, RelayEnabled, RelayDisabled, config.relayFeaturePref);
|
||||
super(RelayOffered, RelayEnabled, RelayDisabled, gConfig.relayFeaturePref);
|
||||
Services.telemetry.setEventRecordingEnabled("relay_integration", true);
|
||||
// Update the config when the signon.firefoxRelay.base_url pref is changed.
|
||||
// This is added mainly for tests.
|
||||
Services.prefs.addObserver(
|
||||
"signon.firefoxRelay.base_url",
|
||||
this.updateConfig
|
||||
);
|
||||
}
|
||||
|
||||
get learnMoreUrl() {
|
||||
return config.learnMoreURL;
|
||||
return gConfig.learnMoreURL;
|
||||
}
|
||||
|
||||
updateConfig() {
|
||||
const newBaseUrl = Services.prefs.getStringPref(
|
||||
"signon.firefoxRelay.base_url"
|
||||
);
|
||||
gConfig.addressesUrl = newBaseUrl + `relayaddresses/`;
|
||||
gConfig.profilesUrl = newBaseUrl + `profiles/`;
|
||||
}
|
||||
|
||||
async autocompleteItemsAsync({ origin, scenarioName, hasInput }) {
|
||||
|
|
|
@ -6,6 +6,7 @@ support-files =
|
|||
form_basic.html
|
||||
form_basic_iframe.html
|
||||
form_basic_login.html
|
||||
form_basic_signup.html
|
||||
form_basic_no_username.html
|
||||
formless_basic.html
|
||||
form_multipage.html
|
||||
|
@ -151,3 +152,4 @@ skip-if = os == "android"
|
|||
[browser_autocomplete_disabled_readonly_passwordField.js]
|
||||
support-files =
|
||||
form_disabled_readonly_passwordField.html
|
||||
[browser_relay_telemetry.js]
|
|
@ -0,0 +1,511 @@
|
|||
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
|
||||
const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
|
||||
const { getFxAccountsSingleton } = ChromeUtils.import(
|
||||
"resource://gre/modules/FxAccounts.jsm"
|
||||
);
|
||||
const { FirefoxRelayTelemetry } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/FirefoxRelayTelemetry.mjs"
|
||||
);
|
||||
|
||||
const gFxAccounts = getFxAccountsSingleton();
|
||||
let gRelayACOptionsTitles;
|
||||
let gHttpServer;
|
||||
|
||||
const TEST_URL_PATH = `https://example.org${DIRECTORY_PATH}form_basic_signup.html`;
|
||||
|
||||
const MOCK_MASKS = [
|
||||
{
|
||||
full_address: "email1@mozilla.com",
|
||||
description: "Email 1 Description",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
full_address: "email2@mozilla.com",
|
||||
description: "Email 2 Description",
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
full_address: "email3@mozilla.com",
|
||||
description: "Email 3 Description",
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const SERVER_SCENARIOS = {
|
||||
free_tier_limit: {
|
||||
"/relayaddresses/": {
|
||||
POST: (request, response) => {
|
||||
response.setStatusLine(request.httpVersion, 403);
|
||||
response.write(JSON.stringify({ error_code: "free_tier_limit" }));
|
||||
},
|
||||
GET: (_, response) => {
|
||||
response.write(JSON.stringify(MOCK_MASKS));
|
||||
},
|
||||
},
|
||||
},
|
||||
unknown_error: {
|
||||
"/relayaddresses/": {
|
||||
default: (request, response) => {
|
||||
response.setStatusLine(request.httpVersion, 408);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
default: {
|
||||
default: (request, response) => {
|
||||
response.setStatusLine(request.httpVersion, 200);
|
||||
response.write(JSON.stringify({ foo: "bar" }));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const simpleRouter = scenarioName => (request, response) => {
|
||||
const routeHandler =
|
||||
SERVER_SCENARIOS[scenarioName][request._path] ?? SERVER_SCENARIOS.default;
|
||||
const methodHandler =
|
||||
routeHandler?.[request._method] ??
|
||||
routeHandler.default ??
|
||||
SERVER_SCENARIOS.default.default;
|
||||
methodHandler(request, response);
|
||||
};
|
||||
const setupServerScenario = (scenarioName = "default") =>
|
||||
gHttpServer.registerPrefixHandler("/", simpleRouter(scenarioName));
|
||||
|
||||
const setupRelayScenario = async scenarioName => {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["signon.firefoxRelay.feature", scenarioName]],
|
||||
});
|
||||
Services.telemetry.clearEvents();
|
||||
};
|
||||
|
||||
const waitForEvents = async expectedEvents =>
|
||||
TestUtils.waitForCondition(
|
||||
() => {
|
||||
const snapshots = Services.telemetry.snapshotEvents(
|
||||
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
|
||||
false
|
||||
);
|
||||
|
||||
return (snapshots.parent?.length ?? 0) >= (expectedEvents.length ?? 0);
|
||||
},
|
||||
"Wait for telemetry to be collected",
|
||||
100,
|
||||
100
|
||||
);
|
||||
|
||||
async function assertEvents(expectedEvents) {
|
||||
// To avoid intermittent failures, we wait for telemetry to be collected
|
||||
await waitForEvents(expectedEvents);
|
||||
const events = TelemetryTestUtils.getEvents(
|
||||
{ category: "relay_integration" },
|
||||
{ process: "parent" }
|
||||
);
|
||||
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
const keysInExpectedEvent = Object.keys(expectedEvents[i]);
|
||||
keysInExpectedEvent.forEach(key => {
|
||||
const assertFn =
|
||||
typeof events[i][key] === "object"
|
||||
? Assert.deepEqual.bind(Assert)
|
||||
: Assert.equal.bind(Assert);
|
||||
assertFn(
|
||||
events[i][key],
|
||||
expectedEvents[i][key],
|
||||
`Key value for ${key} should match`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function openRelayAC(browser) {
|
||||
// In rare cases, especially in chaos mode in verify tests, some events creep in.
|
||||
// Clear them out before we start.
|
||||
Services.telemetry.clearEvents();
|
||||
const popup = document.getElementById("PopupAutoComplete");
|
||||
await openACPopup(popup, browser, "#form-basic-username");
|
||||
const popupItem = document
|
||||
.querySelector("richlistitem")
|
||||
.getAttribute("ac-label");
|
||||
const popupItemTitle = JSON.parse(popupItem).title;
|
||||
|
||||
Assert.ok(
|
||||
gRelayACOptionsTitles.some(title => title.value === popupItemTitle),
|
||||
"AC Popup has an item Relay option shown in popup"
|
||||
);
|
||||
|
||||
const promiseHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
|
||||
popup.firstChild.getItemAtIndex(0).click();
|
||||
await promiseHidden;
|
||||
}
|
||||
|
||||
add_setup(async function() {
|
||||
gHttpServer = new HttpServer();
|
||||
setupServerScenario();
|
||||
|
||||
gHttpServer.start(-1);
|
||||
|
||||
const API_ENDPOINT = `http://localhost:${gHttpServer.identity.primaryPort}/`;
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["signon.firefoxRelay.feature", "available"],
|
||||
["signon.firefoxRelay.base_url", API_ENDPOINT],
|
||||
],
|
||||
});
|
||||
|
||||
sinon.stub(gFxAccounts, "hasLocalSession").returns(true);
|
||||
sinon
|
||||
.stub(gFxAccounts.constructor.config, "isProductionConfig")
|
||||
.returns(true);
|
||||
sinon.stub(gFxAccounts, "getOAuthToken").returns("MOCK_TOKEN");
|
||||
sinon.stub(gFxAccounts, "getSignedInUser").returns({
|
||||
email: "example@mozilla.com",
|
||||
});
|
||||
|
||||
const canRecordExtendedOld = Services.telemetry.canRecordExtended;
|
||||
Services.telemetry.canRecordExtended = true;
|
||||
Services.telemetry.clearEvents();
|
||||
Services.telemetry.setEventRecordingEnabled("relay_integration", true);
|
||||
|
||||
gRelayACOptionsTitles = await new Localization([
|
||||
"browser/firefoxRelay.ftl",
|
||||
]).formatMessages([
|
||||
"firefox-relay-opt-in-title",
|
||||
"firefox-relay-generate-mask-title",
|
||||
]);
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
await new Promise(resolve => {
|
||||
gHttpServer.stop(function() {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
Services.telemetry.setEventRecordingEnabled("relay_integration", false);
|
||||
Services.telemetry.clearEvents();
|
||||
Services.telemetry.canRecordExtended = canRecordExtendedOld;
|
||||
sinon.restore();
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_pref_toggle() {
|
||||
await setupRelayScenario("available");
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: "about:preferences#privacy",
|
||||
},
|
||||
async function(browser) {
|
||||
const relayIntegrationCheckbox = content.document.querySelector(
|
||||
"checkbox#relayIntegration"
|
||||
);
|
||||
relayIntegrationCheckbox.click();
|
||||
relayIntegrationCheckbox.click();
|
||||
await assertEvents([
|
||||
{ object: "pref_change", method: "enabled" },
|
||||
{ object: "pref_change", method: "disabled" },
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_popup_option_optin_enabled() {
|
||||
await setupRelayScenario("available");
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: TEST_URL_PATH,
|
||||
},
|
||||
async function(browser) {
|
||||
await openRelayAC(browser);
|
||||
const notificationPopup = document.getElementById("notification-popup");
|
||||
const notificationShown = BrowserTestUtils.waitForPopupEvent(
|
||||
notificationPopup,
|
||||
"shown"
|
||||
);
|
||||
const notificationHidden = BrowserTestUtils.waitForPopupEvent(
|
||||
notificationPopup,
|
||||
"hidden"
|
||||
);
|
||||
|
||||
await notificationShown;
|
||||
|
||||
notificationPopup
|
||||
.querySelector("button.popup-notification-primary-button")
|
||||
.click();
|
||||
|
||||
await notificationHidden;
|
||||
|
||||
await BrowserTestUtils.waitForEvent(
|
||||
ConfirmationHint._panel,
|
||||
"popuphidden"
|
||||
);
|
||||
|
||||
await assertEvents([
|
||||
{
|
||||
object: "offer_relay",
|
||||
method: "shown",
|
||||
extra: { is_relay_user: "true", scenario: "SignUpFormScenario" },
|
||||
},
|
||||
{
|
||||
object: "offer_relay",
|
||||
method: "clicked",
|
||||
extra: { is_relay_user: "true", scenario: "SignUpFormScenario" },
|
||||
},
|
||||
{ object: "opt_in_panel", method: "shown" },
|
||||
{ object: "opt_in_panel", method: "enabled" },
|
||||
{
|
||||
object: "fill_username",
|
||||
method: "shown",
|
||||
extra: { error_code: "0" },
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_popup_option_optin_postponed() {
|
||||
await setupRelayScenario("available");
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: TEST_URL_PATH,
|
||||
},
|
||||
async function(browser) {
|
||||
await openRelayAC(browser);
|
||||
const notificationPopup = document.getElementById("notification-popup");
|
||||
const notificationShown = BrowserTestUtils.waitForPopupEvent(
|
||||
notificationPopup,
|
||||
"shown"
|
||||
);
|
||||
const notificationHidden = BrowserTestUtils.waitForPopupEvent(
|
||||
notificationPopup,
|
||||
"hidden"
|
||||
);
|
||||
|
||||
await notificationShown;
|
||||
|
||||
notificationPopup
|
||||
.querySelector("button.popup-notification-secondary-button")
|
||||
.click();
|
||||
|
||||
await notificationHidden;
|
||||
|
||||
await assertEvents([
|
||||
{ object: "offer_relay", method: "shown" },
|
||||
{ object: "offer_relay", method: "clicked" },
|
||||
{ object: "opt_in_panel", method: "shown" },
|
||||
{ object: "opt_in_panel", method: "postponed" },
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_popup_option_optin_disabled() {
|
||||
await setupRelayScenario("available");
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: TEST_URL_PATH,
|
||||
},
|
||||
async function(browser) {
|
||||
await openRelayAC(browser);
|
||||
const notificationPopup = document.getElementById("notification-popup");
|
||||
const notificationShown = BrowserTestUtils.waitForPopupEvent(
|
||||
notificationPopup,
|
||||
"shown"
|
||||
);
|
||||
const notificationHidden = BrowserTestUtils.waitForPopupEvent(
|
||||
notificationPopup,
|
||||
"hidden"
|
||||
);
|
||||
|
||||
await notificationShown;
|
||||
const menupopup = notificationPopup.querySelector("menupopup");
|
||||
const menuitem = menupopup.querySelector("menuitem");
|
||||
|
||||
menuitem.click();
|
||||
await notificationHidden;
|
||||
|
||||
await assertEvents([
|
||||
{ object: "offer_relay", method: "shown" },
|
||||
{ object: "offer_relay", method: "clicked" },
|
||||
{ object: "opt_in_panel", method: "shown" },
|
||||
{ object: "opt_in_panel", method: "disabled" },
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_popup_option_fillusername() {
|
||||
await setupRelayScenario("enabled");
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: TEST_URL_PATH,
|
||||
},
|
||||
async function(browser) {
|
||||
await openRelayAC(browser);
|
||||
await BrowserTestUtils.waitForEvent(
|
||||
ConfirmationHint._panel,
|
||||
"popuphidden"
|
||||
);
|
||||
await assertEvents([
|
||||
{ object: "fill_username", method: "shown" },
|
||||
{
|
||||
object: "fill_username",
|
||||
method: "clicked",
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_fillusername_free_tier_limit() {
|
||||
await setupRelayScenario("enabled");
|
||||
setupServerScenario("free_tier_limit");
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: TEST_URL_PATH,
|
||||
},
|
||||
async function(browser) {
|
||||
await openRelayAC(browser);
|
||||
|
||||
const notificationPopup = document.getElementById("notification-popup");
|
||||
const notificationShown = BrowserTestUtils.waitForPopupEvent(
|
||||
notificationPopup,
|
||||
"shown"
|
||||
);
|
||||
const notificationHidden = BrowserTestUtils.waitForPopupEvent(
|
||||
notificationPopup,
|
||||
"hidden"
|
||||
);
|
||||
|
||||
await notificationShown;
|
||||
notificationPopup.querySelector(".reusable-relay-masks button").click();
|
||||
await notificationHidden;
|
||||
|
||||
await assertEvents([
|
||||
{ object: "fill_username", method: "shown" },
|
||||
{
|
||||
object: "fill_username",
|
||||
method: "clicked",
|
||||
},
|
||||
{
|
||||
object: "fill_username",
|
||||
method: "shown",
|
||||
extra: { error_code: "free_tier_limit" },
|
||||
},
|
||||
{
|
||||
object: "reuse_panel",
|
||||
method: "shown",
|
||||
},
|
||||
{
|
||||
object: "reuse_panel",
|
||||
method: "reuse_mask",
|
||||
},
|
||||
]);
|
||||
|
||||
await SpecialPowers.spawn(browser, [], async function() {
|
||||
const username = content.document.getElementById("form-basic-username");
|
||||
Assert.equal(
|
||||
username.value,
|
||||
"email1@mozilla.com",
|
||||
"Username field should be filled with the first mask"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_fillusername_error() {
|
||||
await setupRelayScenario("enabled");
|
||||
setupServerScenario("unknown_error");
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: TEST_URL_PATH,
|
||||
},
|
||||
async function(browser) {
|
||||
await openRelayAC(browser);
|
||||
|
||||
const notificationPopup = document.getElementById("notification-popup");
|
||||
const notificationShown = BrowserTestUtils.waitForPopupEvent(
|
||||
notificationPopup,
|
||||
"shown"
|
||||
);
|
||||
|
||||
await notificationShown;
|
||||
Assert.equal(
|
||||
notificationPopup.querySelector("popupnotification").id,
|
||||
"relay-integration-error-notification",
|
||||
"Error message should be displayed"
|
||||
);
|
||||
|
||||
await assertEvents([
|
||||
{ object: "fill_username", method: "shown" },
|
||||
{
|
||||
object: "fill_username",
|
||||
method: "clicked",
|
||||
},
|
||||
{
|
||||
object: "reuse_panel",
|
||||
method: "shown",
|
||||
extra: { error_code: "408" },
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_auth_token_error() {
|
||||
setupRelayScenario("enabled");
|
||||
gFxAccounts.getOAuthToken.restore();
|
||||
const oauthTokenStub = sinon.stub(gFxAccounts, "getOAuthToken").throws();
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: TEST_URL_PATH,
|
||||
},
|
||||
async function(browser) {
|
||||
await openRelayAC(browser);
|
||||
const notificationPopup = document.getElementById("notification-popup");
|
||||
const notificationShown = BrowserTestUtils.waitForPopupEvent(
|
||||
notificationPopup,
|
||||
"shown"
|
||||
);
|
||||
const notificationHidden = BrowserTestUtils.waitForPopupEvent(
|
||||
notificationPopup,
|
||||
"hidden"
|
||||
);
|
||||
|
||||
await notificationShown;
|
||||
|
||||
notificationPopup
|
||||
.querySelector("button.popup-notification-primary-button")
|
||||
.click();
|
||||
|
||||
await notificationHidden;
|
||||
|
||||
await assertEvents([
|
||||
{
|
||||
object: "fill_username",
|
||||
method: "shown",
|
||||
extra: { error_code: "0" },
|
||||
},
|
||||
{
|
||||
object: "fill_username",
|
||||
method: "clicked",
|
||||
extra: { error_code: "0" },
|
||||
},
|
||||
{
|
||||
object: "fill_username",
|
||||
method: "shown",
|
||||
extra: { error_code: "418" },
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
oauthTokenStub.restore();
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
<form id="form-basic-signup">
|
||||
<input id="form-basic-username" name="username">
|
||||
<input id="form-basic-password" name="password" type="password" autocomplete="new-password">
|
||||
<input id="form-basic-submit" type="submit" value="sign up">
|
||||
</form>
|
||||
|
||||
</body></html>
|
Загрузка…
Ссылка в новой задаче