skip to Main Content

I’m working on writing a script to delete posts from a Facebook Group, since Facebook’s Graph API won’t allow a developer to do so unless the posts were made from the developer’s account.

So far, I have been able to log into Facebook, then navigate to the desired group page. From there I can get the XPath for each post visible on the page (using the selector a[data-testid='post_chevron_button']). My script fails while trying to call this.click() on each XPath selector.

My current script is as follows:

phantom.casperTest = true;
var x = require('casper').selectXPath;
var casper = require('casper').create({   
    verbose: true,
    pageSettings: {
         loadImages:  false,
         loadPlugins: false,
         userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4'
    }
});

// print out all the messages in the headless browser context
casper.on('remote.message', function(msg) {
    this.echo('remote message caught: ' + msg);
});

// print out all the messages in the headless browser context
casper.on("page.error", function(msg, trace) {
    this.echo("Page Error: " + msg, "ERROR");
});

var url = 'http://www.facebook.com/';

casper.start(url, function() {
    console.log("page loaded");
    this.test.assertExists('form#login_form', 'form is found');
    this.fill('form#login_form', { 
        email: '{email}',
        pass: '{password}'
    }, true);
    this.click('#u_0_q');
    this.wait(1000, function() {
        this.echo("Capturing image of page after login.");
        this.capture('loggedin.png');
    });
});

casper.thenOpen('https://www.facebook.com/groups/{group-id}/', function() {
    this.echo(this.getTitle());
    this.wait(1000, function() {
        this.capture('group.png');
    });

    var elements = casper.getElementsInfo("a[data-testid='post_chevron_button']");

    var index = 1;
    elements.forEach(function(element){
        var xpath = '//*[@id="' + element.attributes["id"] + '"]';
        console.log(xpath);
        this.click(x(xpath));
        this.wait(100, function() {
            this.capture('chevronlink' + index + '.png');
        });
        index++;
    });
});

casper.run();

When the script gets to this.click(x(xpath)); I get the error message TypeError: undefined is not a constructor (evaluating 'this.click(x(xpath))'). If I simply replace the last bit of code that creates an array and iterates through it with this.click("a[data-testid='post_chevron_button']");, my script has no problem.

Does anyone know what CasperJS doesn’t like about calling click() with the XPath selector? XPath appears to be a valid selector going off of CasperJS’s docs.

UPDATE

I’ve updated the title of the question to more accurately describe the desired result.

Per dasmelch‘s advice, I’ve reworked the script a bit and incorporated this bit into the script instead (after the casper.thenOpen portion):

casper.then(function() {
  var elements = casper.getElementsAttribute("a[data-
testid='post_chevron_button']", 'id');
  while (elements.length > 0) {
    // get always the last element with target id
    element = elements.pop();
    (function(element) {
      var xpath = '//*[@id="' + element + '"]';
      console.log(xpath);
      // do it step by step
      casper.then(function() {
        this.click(x(xpath));
      });
      casper.then(function() {
        this.capture('chevronlink' + element + '.png');
      });
      // go back to the page with the links (if necessary)
      casper.then(function() {
        casper.back();
      });
    })(element);
  };
});

I now get this error: Cannot dispatch mousedown event on nonexistent selector: xpath selector: //*[@id="u_0_47"].

Last night, I decided to go about it a little differently. I got closer to the desired end result, but now CasperJS and/or PhantomJS is having trouble finding the elements that are present in the dropdown after clicking the post_chevron_button. Here is what I ended up with (everything prior to casper.thenOpen remains the same in the script shown originally):

casper.thenOpen('https://www.facebook.com/groups/{group-id}/', function() {
    this.echo(this.getTitle());
    this.wait(1000, function() {
        this.capture('group.png');
    });

    var elements = casper.getElementsInfo("a[data-
    testid='post_chevron_button']");
    while (elements.length > 0) {
        this.click("a[data-testid='post_chevron_button']");
        this.wait(1000, function() {
            this.capture('chevron_click.png');
            console.log("chevron_click.png saved");
        });
        var chevronLinks = casper.getElementsInfo("a[ajaxify]")
        console.log("Found " + chevronLinks.length + " elements with ajaxify attribute.");
        var chevronLinksIndex = 1;
        chevronLinks.forEach(function(element){
            var ajaxifyValue = element.attributes["ajaxify"];
            console.log(ajaxifyValue);
            if (ajaxifyValue.indexOf("delete.php?group_id={group-id}") !== -1) {
                this.click("a[ajaxify='"+ajaxifyValue+"']");
                this.wait(100, function(){
                    this.capture('deletePost' + chevronLinksIndex);
                });
                chevronLinksIndex++;
            }
        });
        if (chevronLinksIndex === 1) {
            break;
        }
        elements = casper.getElementsInfo("a[data-testid='post_chevron_button']");
    } 
});

I know there should be an element that contains an ajaxify attribute with the value I’m searching for (because stepping through it in a browser myself shows the element after the click on a[data-testid='post_chevron_button']), but Casper cannot find it. Not only that, my chevron_click.png image file should be updating on each run of this script, but it’s not.

Some of the code execution is not happening in order. For instance, the logging of ajaxify attribute values is happening in the console prior to seeing chevron_click.png saved. This may be expected, but unfortunately I don’t have a lot of JS experience. This execution order problem may explain why my search for the necessary element is not returning what I expect.

Here is an example of the element needing clicked for deletion of a post:

<a class="_54nc" href="#" rel="async-post" 
ajaxify="/ajax/groups/mall/delete.php?group_id={group-id}&amp;message_id=806608486110204&amp;story_dom_id=mall_post_806608486110204%3A6%3A0&amp;entstory_context=%7B%22last_view_time%22%3A1495072771%2C%22fbfeed_context%22%3Atrue%2C%22location_type%22%3A2%2C%22outer_object_element_id%22%3A%22mall_post_806608486110204%3A6%3A0%22%2C%22object_element_id%22%3A%22mall_post_806608486110204%3A6%3A0%22%2C%22is_ad_preview%22%3Afalse%2C%22is_editable%22%3Afalse%2C%22mall_how_many_post_comments%22%3A2%2C%22bump_reason%22%3A0%2C%22story_width%22%3A502%2C%22shimparams%22%3A%7B%22page_type%22%3A16%2C%22actor_id%22%3A664025626%2C%22story_id%22%3A806608486110204%2C%22ad_id%22%3A0%2C%22_ft_%22%3A%22%22%2C%22location%22%3A%22group%22%7D%2C%22story_id%22%3A%22u_0_21%22%2C%22caret_id%22%3A%22u_0_22%22%7D&amp;surface=group_post_chevron"
role="menuitem"><span><span class="_54nh"><div class="_41t5"><i
class="_41t7 img sp_gJvT8CoKHU- sx_0f12ae"></i><i class="_41t8 img
sp_s36yWP_7MD_ sx_7e9f7d"></i>Delete Post</div></span></span></a>

2

Answers


  1. Chosen as BEST ANSWER

    I was able to accomplish what I was trying to do with the Selenium 2 API for .NET.

    The solution code is below:

    class Program
    {
        static void Main(string[] args)
        {
            var options = new ChromeOptions();
            options.AddUserProfilePreference("profile.default_content_setting_values.notifications", 2);
    
            using (IWebDriver driver = new ChromeDriver(options))
            {
                // Maximize window
                driver.Manage().Window.Maximize();
    
                // Log into Facebook
                driver.Navigate().GoToUrl("http://www.facebook.com/");
                driver.FindElement(By.Id("email")).SendKeys("username");
                driver.FindElement(By.Id("pass")).SendKeys("password");
                driver.FindElement(By.Id("pass")).SendKeys(Keys.Enter);
    
                driver.Navigate().GoToUrl("https://www.facebook.com/groups/{group-id}/");
                var chevronPostLinks = driver.FindElements(By.XPath("//a[@data-testid='post_chevron_button']"));
                chevronPostLinks.FirstOrDefault().Click();
                Thread.Sleep(1000);
                var deletePostElements = driver.FindElements(By.XPath("//a[contains(@ajaxify,'delete.php?group_id={group-id}')]"));
                while (deletePostElements.Count > 0 && chevronPostLinks.Count > 0)
                {
                    Thread.Sleep(1000);
                    deletePostElements.Where(x => x.Displayed == true).FirstOrDefault().Click();
                    Thread.Sleep(1000);
                    driver.FindElement(By.ClassName("layerConfirm")).Click();
    
                    Thread.Sleep(2000);
                    chevronPostLinks = driver.FindElements(By.XPath("//a[@data-testid='post_chevron_button']"));
                    if (chevronPostLinks.Count > 0)
                    {
                        chevronPostLinks.FirstOrDefault().Click();
                    }
                    else
                    {
                        driver.Navigate().GoToUrl("https://www.facebook.com/groups/{group-id}/");
                        chevronPostLinks = driver.FindElements(By.XPath("//a[@data-testid='post_chevron_button']"));
                        chevronPostLinks.FirstOrDefault().Click();
                    }
                    Thread.Sleep(1000);
                    deletePostElements = driver.FindElements(By.XPath("//a[contains(@ajaxify,'delete.php?group_id={group-id}')]"));
                }
            }
        }
    }
    

    There are some improvements I'd like to make, like using Selenium to wait for elements to be visible instead of using Thread.Sleep(), but it's working just fine for my purpose.


  2. You do the xpath stuff correct, but it seems the method forEach is not working for this.
    You can grab directly the id of all these elements with casper.getElementsAttribute an iterate easy with a while loop throw that them more easily like that:

    ...
    casper.thenOpen('https://www.facebook.com/groups/{group-id}/', function() {
      this.echo(this.getTitle());
      this.wait(1000, function() {
        this.capture('group.png');
      });
    });
    // do a while loop with where you can use  every single element and jump back
    casper.then(function() {
      var elements = casper.getElementsAttribute("a[data-testid='post_chevron_button']", 'id');
      while (elements.length > 0) {
        // get always the last element with target id
        element = elements.pop();
        (function(element) {
          var xpath = '//*[@id="' + element + '"]';
          console.log(xpath);
          // do it step by step
          casper.then(function() {
            this.click(x(xpath));
          });
          casper.then(function() {
            this.capture('chevronlink' + element + '.png');
          });
          // go back to the page with the links (if necessary)
          casper.then(function() {
            casper.back();
          });
        })(element);
      };
    });
    ...
    

    Without looking at FB, i guess you have to go back (casper.back) to the site where the links (elements) are.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search