Posting a Tweet using Playwright
To create content on social media I chose to simulate humans. Instead of trying to use multiple API’s I’m using Playwright to login and post on these sites. Here’s how I do it for Twitter/X.
Steps
The code is really simple but I had to overcome a couple of challenges. I’ll explain them in each step. The steps were:
- Get a Playwright Page
- Log In
- Post
Get a Playwrite Page
First, let me share that I never used Playwright before. It has concepts like browser
, context
and page
. The page
is what we use to navigate to a website and simulate behaviour.
The context represents a state of a browser. For example, if you login in a webiste using one page and then ask the context to give you another page, the new page will have you logged in already, because the context is shared across pages.
A great feature of Playwright is the ability to store a context’s state so it can be loaded again later. In our previous example, that means you can save the state after login and close the page and close the context. Then, you can ask the browser for a new context but using the state you stored. When you create a new page you will be logged in already.
Maybe some code helps
const authFile = "playwright/.auth/twitter.json";
export async function getUnauthenticatedPage() {
const browser = await chromium.launch({
timeout: 60000,
headless: false,
slowMo: 1000,
});
const context = await browser.newContext({
...devices["Desktop Chrome"],
});
const page = await context.newPage();
return {
page,
close: async () => {
await page.close();
await context.close();
await browser.close();
},
};
}
In the code above we launch a browser instance, ask it to create a new context and ask the context to create a new page.
Notice that the method is called getUnauthenticatedPage
. Now, let’s see how to log in.
Log In
Logging in is simple but I struggled a bit getting it to work. Let’s look at the code:
async function doLogin(page: Page, user: string, password: string) {
await page.goto("https://twitter.com/i/flow/login");
// type username
const userInput = '//input[@autocomplete="username"]';
await page.fill(userInput, user);
// click next
await page.click("//span[contains(text(), 'Next')]");
// type password
const passwordInput = '//input[@autocomplete="current-password"]';
await page.fill(passwordInput, password);
// click login
await page.click("//span[contains(text(), 'Log in')]");
// wait for login
await page.waitForURL("https://x.com/home");
}
I found the code on this TikTok. Pretty straight forward. It navigates to the login page, fills the username, clicks Next, fills the password and clicks Log In.
Look at the screenshot below and tell me where the Log In button is.
Hard to see, right? Well, the button is hidden behind the welcome to x.com banner. And since we can’t see it, Playwright can’t see it either. I spent several minutes trying to “find” the X to close the banner and reveal the button. I couldn’t. Maybe a more experienced Playwright developer would have done it. My solution was to change the device.
// from
const context = await browser.newContext({
...devices["iPhone 11"],
});
// to
const context = await browser.newContext({
...devices["Desktop Chrome"],
});
This is how it looks
Now that we can see the Log in button, so can Playwright and the login suceeded 🎉.
Now that we are logged in, we can tweet. However, I didn’t want to log-in-then-tweet each time I wanted to post something. Twitter/X might perceive it as a weird behaviour and the account could be cancelled or something. So, before tweeting, I stored the context to be reused in future sessions.
const { page, close } = await getUnauthenticatedPage();
await doLogin(page, user, password);
await page.context().storageState({ path: authFile });
await close();
That’s it. With our context stored we can proceed to tweeting from another page.
Tweet
Notice that we logged in used the getUnauthenticatedPage
method. Since we saved an authenticated context state we can load it to getAuthenticatedPage
.
export async function getAuthenticatedPage() {
// ... Same as getUnauthenticatedPage
const context = await browser.newContext({
...devices["Desktop Chrome"],
storageState: authFile, // This line is new
});
// ... Same as getUnauthenticatedPage
}
Above you can see where we load the saved context state. With it, we create a new page which is logged in and we can post a tweet.
async function postTweet(page: Page, text: string) {
// open twitter
await page.goto("https://x.com/home");
// click away the cookie banner
await page.click("//span[contains(text(), 'Refuse non-essential cookies')]");
// click tweet
await page.click("a[href='/compose/post']");
// type tweet
await page.fill(
"//div[@data-viewportview='true']//div[@class='DraftEditor-editorContainer']/div[@role='textbox']",
text
);
// click post
await page.click("//span[contains(text(), 'Post')]");
// wait for tweet
await page.waitForURL("https://x.com/home");
await page.waitForTimeout(r(1000, 2500));
await simulateRandomBehaviour(page);
}
Again, pretty straight forward. Go to x.com, click on the cookie banner, click on post, type the message and click on post. If you read the code carefully, you will notice a click on the cookie banner step. That was an addition to the code I used as reference. Since I am in Europe, I get prompted with a banner to accept/reject cookies. It was a small obstacle but easier to overcome. I just found the right button and clicked the banner away.
.
That’s it folks! A bonus piece of code: Random scrolling :)
async function simulateRandomBehaviour(page: Page) {
// simulate scrolling
const times = r(2, 5);
for (let i = 0; i < times; i++) {
await page.mouse.wheel(0, r(100, 500));
await page.waitForTimeout(r(1000, 2500));
}
}
The r
function you see there just gets a random integer within the range.
The code
Find the code here and a video demo below. Thanks for reading!