Why does Puppeteer not save new textarea value?

291 Views Asked by At

This is a Puppeteer script running on Apify, designed to enter values into a form and save it. The fields are an input and a textarea, respectively.

The problem is - while the values of both fields are getting successfully updated in the front end (evidenced by taking screenshots), the value of the textarea is not actually saved when the Save <button> is .clicked, even though the input value alone is saved.

Why is this happening, and how do I overcome it?

enter image description here

I have tried several different methods of entering the textarea text:

  • Unlike the input, I have opted against using .type for the textarea (even though it worked fine for small amounts of text) due to the volume of the text being entered - it was causing timeouts.
  • I have tried using wait times, artificially changing focus and clicking into the textarea followed by .typeing an arbitrary value. In no circumstance does the textarea value get saved.
  • I have read on Stackoverflow that textareas should be edited via .innerText rather than .value, but this seems to make no difference in my case.

FYI - the form only becomes visible and active when the user/script clicks an Edit button (before this, the page had showed the form fields as static content). After the single Edit is clicked, two Save buttons appear (.edit-video-info becomes .save-video-info At the top, and then another .save-video-info appears. But a) the script simply uses the first one found and b) the Save operation is otherwise successful, as borne out by the input value getting saved.

import { Actor } from 'apify';
import { PuppeteerCrawler } from 'crawlee'; // "puppeteer" must be included in package.json dependencies

await Actor.init();
console.log('Actor initialised.')
console.log('Welcome. Only run if input was passed...')

const input = await Actor.getInput();

if (input) {
    console.log('Input was passed...')
    console.log(input);

    // Create an instance of the PuppeteerCrawler class - a crawler
    // that automatically loads the URLs in headless Chrome / Puppeteer.
    const crawler = new PuppeteerCrawler({
        // Here you can set options that are passed to the launchPuppeteer() function.
        launchContext: {
            launchOptions: {
                headless: true,
                // Other Puppeteer options
                defaultViewport: { width: 1800, height: 2100 }
            },
        },
        requestHandlerTimeoutSecs: 180,
        // Stop crawling after several pages
        maxRequestsPerCrawl: 50,

        async requestHandler({ request, page, enqueueLinks }) {

            /************************************
             * Function: Long-text workaround - used for body
             ************************************/
            // workaround cf. https://github.com/puppeteer/puppeteer/issues/4192#issuecomment-475241477
            async function setSelectVal(sel, val) {
                page.evaluate((data) => {
                    return document.querySelector(data.sel).value = data.val
                }, { sel, val })
            }           

            console.log(`Processing ${request.url}`);

            /************************************
             * Authentication
             ************************************/
            let isLoggedIn = false;
            // Is site logged-in? true/false
            const loggedCheck = async (page) => {
                try {
                    await page.waitForSelector('input#user', { timeout: 10000 });
                    console.log('Logged-in: true.');
                    return true;
                }
                catch(err) {
                    console.log('Logged-in: false.');
                    return false;
                }
            };
            isLoggedIn = await loggedCheck(page);

            if (!isLoggedIn) {
                console.log('Need to do login...');
                const SECRET_EMAIL = 'MYEMAILADDRESS'
                const SECRET_PASSWORD = 'MYPASSWORD'
                await page.waitForSelector('#signinButton')
                console.log('Entering email...');
                await page.type('input#email', SECRET_EMAIL)
                console.log('Entering password...');
                await page.type('input#password', SECRET_PASSWORD)
        
                console.log('Clicking #signinButton...');
                await page.click('#signinButton')
                console.log('Clicked #signinButton.');
                isLoggedIn = await loggedCheck(page);
                console.log('Login should now be true.');
                console.log(isLoggedIn);

            }


            /************************************
             * Kicking off...
             ************************************/

            console.log('Do stuff when #video-details loads...');
            await page.waitForSelector('#video-details', { timeout: 30000 })

            // Increase browser size
            await page.setViewport({
            width: 1200,
            height: 2400,
            deviceScaleFactor: 1,
            });

            console.log('Existing video title...');
            console.log(await page.evaluate(() => document.evaluate('normalize-space(//div[@data-test-name="video-name"]/div[2])', document, null, XPathResult.STRING_TYPE, null).stringValue));
            console.log('Echoed.');

            /************************************
             * Edit video details...
             ************************************/

            console.log('Clicking Edit button...');
            await page.click('button[data-test-name=edit-video-info]')
            console.log('Clicked Edit button.');

            /************************************
             * 1. Video title
             ************************************/
            console.log('Wait for input field...');
            await page.waitForSelector('label[data-test-name=video-name-input]', { timeout: 30000 })
            console.log('OK.');
            console.log('Changing video title...')
            if (input.title) {
                console.log('input.title is available')
                const titlebox = await page.$('label[data-test-name=video-name-input] input');
                await titlebox.click({ clickCount: 3 })
                await titlebox.type(input.title);
                console.log('OK')
            } else {
                console.log('No input.title - will not change.')
            }

            /************************************
             * 2. Body text
             ************************************/
            console.log('Wait for body textarea...');
            await page.waitForSelector('label[data-test-name=long-description-textarea] textarea', { timeout: 30000 })
            console.log('OK.');
            console.log('Changing body text...')

            if (input.body) {
                console.log('input.body is available')
                const bodytext = input.body;
                console.log(bodytext);
                
                await setSelectVal('label[data-test-name=long-description-textarea] textarea', bodytext)

                console.log('OK')
            } else {
                console.log('No input.body - will not change.')
            }

            /* Screenshot */
            const screen07typedinput = await page.screenshot();
            await Actor.setValue('screen07typedinput.png', screen07typedinput, { contentType: 'image/png' });



            /************************************
             * Save video details
             ************************************/

            // await page.focus('button[data-test-name=save-video-info]')[1];

            await page.waitForTimeout(3000)

            console.log('Clicking Save button...');
            await page.click('button[data-test-name=save-video-info]')

            console.log('Did stuff');

            /* Screenshot */
            const screen08aftersave = await page.screenshot();
            await Actor.setValue('screen08aftersave.png', screen08aftersave, { contentType: 'image/png' });

            /************************************
             * Logout
             ************************************/
            console.log('Trying logout...')
            await page.goto('https://signin.example.com/logout');
            console.log('Finished.');





        },

        // This function is called if the page processing failed more than maxRequestRetries+1 times.
        failedRequestHandler({ request }) {
            console.log(`Request ${request.url} failed too many times.`);
        },
    });

    // Run the crawler and wait for it to finish.
    // await crawler.run(['https://news.ycombinator.com/']);
    await crawler.run(['https://site.example.com/products/videocloud/media/videos/'+input.assetid]);

    console.log('Crawler finished.');

} else {
    console.log('No input. Abandon.')
}

await Actor.exit();

The button markup is...

<button class="Button-blueSteel-4_1_4-3Skod Button-btn-4_1_4-QuPwe Button-primary-4_1_4-2YCmt Button-small-4_1_4-1Wurh" data-test-name="save-video-info" role="button" tabindex="0" type="button" aria-pressed="true">
<span class>Save</span>
</button>

An id attribute is absent, so I can't use getElementByID.

1

There are 1 best solutions below

0
Robert Andrews On

This works...

  1. I had not .clicked inside the <textarea>. Whilst I had .clicked the <input> field prior to .typeing into it, I had not done this with the <textarea> - because I wasn't using .type there; I was having to set the `.values.

  2. In addition to that, for good measure, I .click Back in to the <input> field.

  3. Also, I `.types an arbitrary value.

I think it was the clicks. Without having clicked the <textarea>, only the apparent .value was getting set.

await page.click('label[data-test-name=long-description-textarea] textarea')
await setSelectVal('label[data-test-name=long-description-textarea] textarea', bodytext)
await page.click('label[data-test-name=long-description-textarea] textarea')
await page.type('label[data-test-name=long-description-textarea] textarea',' ')

await page.click('label[data-test-name=video-name-input] input')