-
Notifications
You must be signed in to change notification settings - Fork 354
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
test: user tests continued [INFENG-455] #9214
Conversation
d64a121
to
1c10053
Compare
✅ Deploy Preview for determined-ui ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
@@ -20,62 +22,11 @@ test.describe('User Management', () => { | |||
await expect(page).toHaveURL(userManagementPage.url); | |||
}); | |||
|
|||
test.skip('Users table count matches admin page users tab', async ({ page }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this moves to lower in the suite and we dont skip anymore
webui/react/playwright.config.ts
Outdated
@@ -23,8 +23,8 @@ export default defineConfig({ | |||
fullyParallel: !!process.env.CI, | |||
|
|||
/* https://playwright.dev/docs/test-timeouts#global-timeout */ | |||
globalTimeout: process.env.PWDEBUG ? 0 : 5 * 60 * 1000, // 3 min unless debugging | |||
timeout: 60000, // TODO [INFENG-628] Users page loads slow so we extend 5 minutes and 1 minute per test until we get an isolated backend | |||
globalTimeout: process.env.PWDEBUG ? 0 : 500_000, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i was getting timeouts because my test suite is getting larger
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #9214 +/- ##
==========================================
- Coverage 44.71% 37.92% -6.80%
==========================================
Files 1271 949 -322
Lines 155397 116000 -39397
Branches 2436 2438 +2
==========================================
- Hits 69487 43993 -25494
+ Misses 85673 71768 -13905
- Partials 237 239 +2
Flags with carried forward coverage won't be shown. Click here to find out more.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overall I like it but sharing users in a session map is probably going to cause race conditions so I think we should still try to have each test only interact with users it creates.
If there is something really tricky like pagination that requires 10+ users we could hold off and see if we can use mocks for that. It would be faster and impervious to environment shenanigans. We can have a mock return 1000 users if we want and we don't have to create or manage those users.
return { | ||
safeName: (baseName: string): string => { | ||
const timestamp = Date.now(); | ||
return `${baseName}${sessionRandomHash}${randIdAlphanumeric()}_${timestamp}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just use a simpler UUID for to avoid collision? Generating random ids is not the fastest operation, and it seems a little weird to do two calls to Math.random() and get a timestamp when a single uuid.v4() is virtually guaranteed not to collide.
I'm also not a fan of putting meaningful information in unique IDs. It always makes it harder to change down the line. If we need to know what tests made which users when, we should have a debug log for that rather than putting it in an ID.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
meaningful information in unique IDs
that's a good point. i always think it's best to name a test case rather than generate it's name from the types of parameters it has, for example.
Could we just use a simpler UUID
the session random hash is specifically useful in situations like this one. using the session hash, I can find all the users i made "during this test session" with
await userManagementPage.search.pwLocator.fill(usernamePrefix + sessionRandomHash);
rather than all the users that just match the prefix, which would collide.
if i had 10 unique ID users, there's no way I could gather them all with a single search unless I used a prefix, but that prefix would be guaranteed to fail on a dirty environment. After the hashes, I figured it would be useful to anyone using a dirty environment to see a timestamp. I agree that we shouldn't be relying on the timestamp for collision or test meaning.
if it's alright with you, I'll throw this explanation into a docstring or readme-- I really need to keep the session hash. I don't know another way around it. Also, it's not 2n
calls to Math.random()
, it's n+1
.
@@ -16,9 +16,11 @@ interface UserArgs { | |||
isAdmin?: boolean; | |||
} | |||
|
|||
// one list of users per test session | |||
const users = new Map<string, User>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This maybe a race condition if it is truly shared across tests. Setting a map is likely not an atomic operation It has to do a Get on the map, then save the map with a new value to the place in memory. If two tests do this at the same time it could go like:
- test1 gets the map
- test2 gets the map
- test1 set's it's user and persists.
- test2 sets it's user and persists, overriding the new user from test1 since the initial GET occurred before the user was added
I think trying to have a list of all users across all tests is not a great practice anyway since if we want to shard the runners in the future this becomes an incomplete list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah this is a good point. ultimately, I only use this list to tear down uses. But I wouldn't want Suite A to teardown users from Suite B. let me think on how to fix this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the case of shared runners, I believe they would each still load their own modules, and so they wouldn't collide. But if one runner was executing multiple test suites at once via threading, then we'd be in trouble.
await expect(row.status.pwLocator).toContainText(isActive ? 'Active' : 'Inactive'); | ||
} | ||
|
||
async deactivateTestUsersOnTable(): Promise<void> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems scary to try to pull all users and deactivate them. It could blow up other tests on shared test environments if we try to wipe them from all tests. Not that many tests will create users right? Can each test that creates users track their users and delete them rather than trying to archive all of them?
(I assumed this used the username session hash from safeUsername() to only remove this test sessions users, but I don't see that in allRowkeys or anywhere else, so now I'm not sure)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah this is a helper method that clicks this button in blue.
I agree it's pretty scary, in the test implementation, I actually check that all the users on the table were made in this session. Maybe i can put something similar into the method itself
await expect(async () => {
expect(await userManagementPage.table.table.filterRows(async (row) => {
return (await row.user.name.pwLocator.textContent())?.indexOf(usernamePrefix) === 0;
})).toHaveLength(10);
}).toPass({timeout: 10000});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we are checking that the test created the user, should we just start with the list of users and only delete those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we just start with the list of users and only delete those
yeah that's what i'm doing with this.
// set pagination to 10
await userManagementPage.table.table.pagination.perPage.pwLocator.click();
await userManagementPage.table.table.pagination.perPage.perPage10.pwLocator.click();
// filter by active users
await userManagementPage.filterStatus.pwLocator.click();
await userManagementPage.filterStatus.activeUsers.pwLocator.click();
// search for users created this session and wait for table stable
await userManagementPage.search.pwLocator.fill(usernamePrefix + sessionRandomHash);
on a clean environment, i could get away with this
await userManagementPage.search.pwLocator.fill(usernamePrefix);
locally, my environment will never be clean, so i include the session hash
* @param {string} obj.selector - Used instead of `defaultSelector` | ||
*/ | ||
export class Nameplate extends NamedComponent { | ||
override defaultSelector: string = '[class^="Nameplate_base"]'; // must be provided |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// must be provided
can we remove this comment? I don't think it adds anything when the selector is non-blank
export class Nameplate extends NamedComponent { | ||
override defaultSelector: string = '[class^="Nameplate_base"]'; // must be provided | ||
|
||
#nameSelector = '.ant-typography:last-of-type'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
last-of-type
seems like a fragile way to define a selector. Do we have a way forward to use another selector? (potentially in a future change if it's involved)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
our way forward would be with a change to hew. i felt clever coming up with this solution though!
await expect(async () => { | ||
await userFixtureSetupTeardown.deactivateTestUsers(); | ||
}).toPass({ timeout: 20_000 }); | ||
await userFixtureSetupTeardown.deactivateAllTestUsers(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this cause problems for other describe blocks if they make a user during the test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's an issue for parallel. And it can't be an issue for single-worker.
https://playwright.dev/docs/test-parallel#parallelize-tests-in-a-single-file
Note that parallel tests are executed in separate worker processes and cannot share any state or global variables. Each test executes all relevant hooks just for itself, including beforeAll and afterAll.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't user fixture pull the users from the table though with this? const ids = await this.userManagementPage.table.table.allRowKeys();
and then on line #145 we set the user to the map.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay so two things. the part you mentioned is deactivateTestUsersOnTable
. table.allRowKeys();
in that function is used to manage the internal state. "physically", the test is using this to set the status in the product.
await this.userManagementPage.table.table.headRow.selectAll.pwLocator.click();
however, this line here is referencing deactivateAllTestUsers
. That method will only go through the users from this test session. It manually searches for each user one at a time, so there shouldn't be a risk of search collision.
for await (const user of users.values()) {
08b4a49
to
f4f5188
Compare
78efd7c
to
bdaf43b
Compare
bdaf43b
to
bf50b15
Compare
a18cf89
to
ed665bb
Compare
Ticket
INFENG-455
INFENG-628
Description
All the remaining user tests from here
Tests are serial for now, but I'll make them parallel as a part of INFENG-658
Test Plan
Checklist
docs/release-notes/
.See Release Note for details.