-
Notifications
You must be signed in to change notification settings - Fork 674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
If localStorage is populated via Role, it's empty when running parallel tests #2782
Comments
Hi, @darina-techery import { Role, Selector, ClientFunction } from 'testcafe';
import rp from 'request-promise';
const saveLocalStorage = ClientFunction (storage => {
window.localStorage.setItem('app_state', storage);
});
const options = {
method: 'POST',
uri: 'https://app.appspector.com/api/auth/login',
body: {
email: '**********',
password: '**********'
},
json: true
};
const userRole = Role('https://app.appspector.com/', async t => {
const body = await rp(options);
const session = JSON.stringify( { session: body, settings: {} });
await saveLocalStorage(session);
await t.navigateTo('https://app.appspector.com/welcome');
}, { preserveUrl: true });
fixture `Should restore local storage correctly`
.beforeEach(async t => {
await t.useRole(userRole);
});
test('Should be logged in', async t => {
await t.click(Selector('div').withText('Welcome to AppSpector'));
});
test('Should be logged in', async t => {
await t.click(Selector('div').withText('Welcome to AppSpector'));
});
test('Should be logged in', async t => {
await t.click(Selector('div').withText('Welcome to AppSpector'));
});
test('Should be logged in', async t => {
await t.click(Selector('div').withText('Welcome to AppSpector'));
});
test('Should be logged in', async t => {
await t.click(Selector('div').withText('Welcome to AppSpector'));
});
test('Should be logged in', async t => {
await t.click(Selector('div').withText('Welcome to AppSpector'));
});
test('Should be logged in', async t => {
await t.click(Selector('div').withText('Welcome to AppSpector'));
});
test('Should be logged in', async t => {
await t.click(Selector('div').withText('Welcome to AppSpector'));
});
test('Should be logged in', async t => {
await t.click(Selector('div').withText('Welcome to AppSpector'));
}); |
Hi @AlexKamaev , I've already tried that with preserveUrl option, and I can see now that the reason may be not in concurrent execution, but in some concurrency issue, because localStorage can appear empty even in a sequential run. I commented on this in another thread and provided a code sample with preserveUrl there: #2475 (comment) |
@darina-techery |
Yes, I tried your test, and it runs ok. I'm trying to find out what makes the difference. My first option is moving |
In my test, I do not use |
Ok, I think I traced the source of this problem via debug output. const authenticateInLocalStorage = async function(authResponse) {
const appState = JSON.stringify({
session: authResponse,
settings: {}
});
console.log("Set app_state: "+appState);
await client.localStorageSet("app_state", appState);
console.log("Wait until app_state is found in localStorage");
await t.expect(client.localStorageGet("app_state")).ok();
console.log("app_state is found: "+await client.localStorageGet("app_state"));
}; In some cases it works. Example of valid output:
But in some cases it results in: 76.03: Set app_state: {"session":{"email":"[email protected]","name":"Yolanda Tillman","accountId":***,"token":"***","agreementAccepted":false,"id":***},"settings":{}}
76.75: Wait until app_state is found in localStorage
77.17: app_state is found: {"settings":{}} This line For the record, my localStorageSet is |
@darina-techery I'm working on the issue and today your site banned me. Could you please help me restore access to the site to continue my research? |
@AlexKamaev our security guys have recently blocked a bunch of IPs. They considered multiple sessions spawning as a kind of DDoS attack. I'm terribly sorry for this misunderstanding. |
@darina-techery thanks, I've mailed it |
@AlexKamaev I've whitelisted your IP. Sorry for inconvenience :) |
@sergeyzenchenko thanks |
I'm sharing a couple of workarounds for those who might encounter the problem before it's fixed:
const authenticateInLocalStorage = async function(localStorageEntry) {
console.debug("Expecting state in localStorage:", localStorageEntry);
let retryCount = 0;
const maxRetries = 10;
const tokenToCheck = localStorageEntry.someToken;
let failed = false;
const authFailed = async (localStorageState): Promise<boolean> => {
return (
//check if localStorage state matches expected one
!localStorageState || localStorageState.someToken !== tokenToCheck
);
};
do {
retryCount++;
await localStorageSet("state", localStorageEntry);
const appStateInLS = await localStorageGet("state");
failed = await authFailed(appStateInLS);
console.debug(
(failed ? "Fail" : "Pass") +
": state in localStorage is " +
appStateInLS
);
} while (retryCount < maxRetries && failed);
if (failed) {
throw new Error(
"Failed to set state in localStorage: expected " +
localStorageEntry +
", but found " +
(await localStorageGet("state"))
);
}
}; |
@darina-techery Here is my tests: import { Role, Selector, ClientFunction, RequestMock, RequestHook } from 'testcafe';
import rp from 'request-promise';
const saveLocalStorage = ClientFunction (storage => {
window.localStorage.setItem('app_state', storage);
});
const options = {
method: 'POST',
uri: 'https://app.appspector.com/api/auth/login',
body: {
email: '[email protected]',
password: '1234567890'
},
json: true
};
const mock = RequestMock()
.onRequestTo(/.*logrocket.*/)
.respond((req, res) => {
res.headers['x-calculated-header'] = 'calculated-value';
res.statusCode = '200';
res.setBody('body');
})
.onRequestTo(/.*google-analytics.*/)
.respond((req, res) => {
res.headers['x-calculated-header'] = 'calculated-value';
res.statusCode = '200';
res.setBody('body');
})
.onRequestTo(/.*mixpanel\.com.*/)
.respond((req, res) => {
res.headers['x-calculated-header'] = 'calculated-value';
res.statusCode = '200';
res.setBody('body');
})
.onRequestTo(/.*countly.*/)
.respond((req, res) => {
res.headers['x-calculated-header'] = 'calculated-value';
res.statusCode = '200';
res.setBody('body');
});
const userRole = Role('https://app.appspector.com/login', async t => {
const body = await rp(options);
const session = JSON.stringify( { session: body, settings: {} });
await saveLocalStorage(session);
await t.wait(5000);
}, { preserveUrl: false });
fixture `Should restore local storage correctly`
.page('https://app.appspector.com/welcome')
.requestHooks(mock);
test('Should be logged in', async t => {
await t.useRole(userRole);
await t.wait(4000)
await t.click(Selector('div').withText('Welcome to AppSpector'));
});
test('Should be logged in', async t => {
await t.useRole(userRole);
await t.wait(4000)
await t.click(Selector('div').withText('Welcome to AppSpector'));
}); |
I tried my tests with this option, but the result was the same.
The next failure, which is a constant one, occurred when t.useRole was not the first command in the test. I obtained the token via REST API requests and used it to send a few requests to create test data (in my case, create a test organization and send an invite to another user which should accept it). const acceptEmail = generators.email();
const acceptRole = Role(
"https://app.appspector.com/welcome",
async () => {
// register via API and set app_state in localStorage;
await registerAs(acceptEmail);
},
{ preserveUrl: true }
);
//I used this workaround, because in some cases it worked, but now it failed:
export const useRoleAndReload = async (t, role: Role) => {
await t.useRole(role).wait(1000);
await client.reload();
};
test
("Accept invite to organization",
async t => {
//create user session via API
const mainUserSession = await usersApi.login(
mainUser.email,
mainUser.password
);
//save token to t.ctx.token
await requests.setToken(mainUserSession.token);
//create organization as main user (token is automatically taken from t.ctx.token)
const organization = await orgsApi.createOrganization();
await invitesApi.inviteMember({
orgId: organization.id,
email: acceptEmail,
role: "member"
});
//now we try to register a user with an email we used to send invite to
await useRoleAndReload(t, acceptRole);
await t
.expect(welcomePage.viewInvites.textContent)
.eql("View 1 Invite", "View Invites button")
}
); This test also fails, as I see login page on the screen. |
@AlexKamaev Oh, I see I still used |
@AlexKamaev I changed the role approach (now tests contain I can also experiment with increasing the |
Since the issue disappeared on my side after disabling the cache, the reason of the issue can be not only in the cache. Could you please run your tests with my packed testcafe versions? One version just logs the requests, the second version has some modifications. Could you share these logs with me?
|
@darina-techery |
@AlexKamaev I have built a sample project which guarantees reproducibility, and it doesn't seem to be connected to requestMocks. The failures look completely random to me, and sometimes changing something in the test might make it look like as if the problem was fixed, but then the localStorage issue emerges again without any further modifications :( But so far I would prefer to leave the sample project private, unless we are unable to resolve the issue without the code being exposed (NDA and stuff). Could you please check your email? I messaged you with some details regarding this investigation. Thanks! |
The issue is solved now. As @AlexKamaev found, the problem was caused by
export const client = {
backupStorage: ClientFunction(appState => {
window.localStorage.setItem("app_state", appState);
return window['%hammerhead%'].sandbox.storageSandbox.backup();
}),
const setUpLocalStorageForRole = async (args, role) => {
//perform setup actions, populate localStorage
const backup = await client.backupStorage(appState)
role.once('initialized', async () => {
role.stateSnapshot.storages = backup;
}); Thanks so much for you help! I think we're settled here. |
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs or feature requests. For TestCafe API, usage and configuration inquiries, we recommend asking them on StackOverflow. |
Are you requesting a feature or reporting a bug?
bug
What is the current behavior?
WHEN tests are executed in parallel (concurrent runs OR multiple browsers)
AND test setup is performed via roles
AND each role populates localStorage to authenticate user
THEN some browser instances have empty localStorage
AND tests fail, because test user is not authorized without respective localStorage entry.
Details:
When user signs in to our application, an
app_state
property is added to localStorage. It contains a unique connection token. User with this token in localStorage is authorized to perform any operations and has access to the whole application.localStorage contain the following entry for authorized user:
If the tests are executed subsequently, it works as expected. But when I launch tests in multiple browsers or in parallel, a number of tests in a set always fail due to empty localStorage (
app_state
property is absent). Current user is considered not signed in, and only login page is accessible to him.Only one browser instance receives properly populated localStorage (probably the one where the role was initialized first?). The other instances have empty localStorages without
app_state
and tokens.What is the expected behavior?
Each browser instance maintains its own localStorage, and if some role has already been initialized for one browser instance, it should be restored properly in the others.
My average scenario looks like this:
(in 'before' hook)
(in test itself)
I expect user to be authorized after step 1 and redirected to the landing page, not login page.
How would you reproduce the current behavior (if this is a bug)?
Provide the test code and the tested page URL (if applicable)
Test code
I authenticate my user using REST API ('request' package) and client function writing to localStorage. It looks like this:
Most tests have the following structure:
The test above will fail trying to interact with welcome page, as login page will be found instead.
Specify your
The text was updated successfully, but these errors were encountered: