saush

Using RSpec and Watir for functional testing in web applications

Posted in Ruby by sausheong on May 18, 2008

I started playing around with RSpec the past week while also fiddling with Watir at around the same time, so it wasn’t a big leap of creativity to combine both together.

RSpec is the popular behavior-driven development test framework written in Ruby while Watir (Web Application Testing In Ruby) is a set of Ruby APIs used to automate Internet Explorer, making use of Ruby’s built-in Win32OLE capabilities.

By itself RSpec can only test Ruby applications or scripts. The immense popularity of Ruby on Rails produced RSpec on Rails but personally I didn’t quite like testing each model, controller and view separately and it is more unit testing than functional testing of an entire system. Watir on its own is an excellent tool to automate navigating through web sites and web applications through the Internet Explorer. However it doesn’t provide the kind of structure and driven directions that a BDD test framework like RSpec does.

When combined together, RSpec + Watir makes real magic in doing functional testing for web applications. However you need to realize that Watir only runs on IE (though there exists FireWatir and SafariWatir, both which runs on other platforms for FireFox and Safari respectively). While this seems terribly restrictive to some, I come to realize that the strength of RSpec + Watir is not in testing browser compatibility but in being able to run through the web application as seen and used by the actual user, through an actual browser. RSpec + Watir produces a synergistic capability where each cannot. The combined tool allows us to test any web application (whether Ruby on Rails or not) and to do it in a BDD way.

Installing RSpec and Watir is very simple. You just need to install the RSpec and Watir gems respectively:

c:\> gem install watir
c:\> gem install rspec

And you’re off! There’re dozens of articles and blogs that describe how each tool can be used on its own so in this post I will just focus on describing how they can be used together (you should try to read up a bit on RSpec and Watir first). For this I will try to ‘test’ Facebook, with the following 2 very simple examples:

Facebook functional testing
- Edit my profile
- It should add new work information
- It should upload a new picture
- Navigate the online friends tab
- It should flash every online friend
- It should send the first online friend found a message

Now, we translate this into a properly nested example group in RSpec:

require 'spec'

describe 'Facebook' do
describe '(edit my profile)' do
it 'should add new work information into my profile'
it 'should upload a new picture into my profile'
end

describe '(navigate the online friends tab)' do
it 'should flash every online friend'
it 'should send the first online friend a message'
end

end

To someone who has never used RSpec (or this is the first time you’re encountering RSpec) this looks facetious but trust me in this, we’ll expand it in a while and you’ll see how powerful it can be. For now suspend your disbelief, save the code into a ruby script named fb1.rb and run the following:

c:\> spec fb1.rb --format specdoc

You will an output like this:

Facebook (edit my profile)
- should add new work information (PENDING: Not Yet Implemented)
- should upload a new picture (PENDING: Not Yet Implemented)

Facebook (navigate the online friends tab)
- should flash every online friend (PENDING: Not Yet Implemented)
- should send the first online friend a message (PENDING: Not Yet Implemented)

Pending:
Facebook (edit my profile) should add new work information (Not Yet Implemented)
Facebook (edit my profile) should upload a new picture (Not Yet Implemented)
Facebook (navigate the online friends tab) should flash every online friend (Not Yet Implemented)
Facebook (navigate the online friends tab) should send the first online friend a message (Not Yet Implemented)

Finished in 0.02 seconds

4 examples, 0 failures, 4 pending

It should be clear now (or at least, less fuzzy) why this is called ‘Behavior Driven Development’. In a real development scenario, we’ll be writing such examples before writing any code and this will tell us how the application should behave. After this is done, we’ll switch back to the code and implement the behavior. I use the term ‘we’ loosely because these examples can be written by the end-user. By doing this, we can ‘describe’ how the application should function.

Now that we have the examples described, let’s fill in the bits. In a development scenario, we would have gone back to developing the application but here we’re just going to continue writing the example.

First, let’s require Watir then add in a before and an after block. Before and after blocks are part of RSpec and allows us to set up the necessary objects for testing. In our case we use the before block to set up IE and log into Facebook, while the after block is used to log out of Facebook and close IE.

require 'spec'
require 'watir'

email = ''
password = ''

describe 'Facebook' do
before :all do
$ie = IE.new
$ie.goto('http://www.facebook.com')
$ie.text_field(:id, 'email').set(email)
$ie.text_field(:id, 'pass').set(password)
$ie.button(:id, 'doquicklogin').click
end

describe '(edit my profile)' do
it 'should add new work information'
it 'should upload a new picture'
end

describe '(navigate the online friends tab)' do
it 'should flash every online friend'
it 'should send the first online friend a message'
end

after :all do
$ie.link(:text, 'logout').click
$ie.close
end
end

We start off in the before block by starting up a new IE browser, then directing it to go to Facebook’s main page. After that we look for two text fields with the id ‘email’ and ‘pass’ respectively and fill it up with email and password accordingly. Once that is done, we click on the login button.

The after block is obvious so I won’t elaborate.

Let’s go to the first example, the ‘edit my profile’ block.


describe '(Editing my profile)' do

it 'should add new work information' do
$ie.link(:after? => $ie.link(:text, 'Profile'), :text => 'edit').click
$ie.link(:text, 'Work').click
$ie.text_field(:name, 'work_history_1_company').set('Way Cool Company')
$ie.text_field(:name, 'work_history_1_position').set('Master of the Universe')
$ie.text_field(:name, 'work_history_1_description').set('Lord and Sovereign of All I See')
$ie.text_field(:name, 'work_history_1_location_sq').set('Singapore')
$ie.checkbox(:name, 'work_history_1_workspan_current').set
$ie.select_list(:name, 'work_history_1_workspan_start_month').set('June')
$ie.select_list(:name, 'work_history_1_workspan_start_year').set('2008')
$ie.button(:id, 'save').click
$ie.text.should include('Changes saved.')
end

it 'should upload a new picture' do
$ie.link(:after? => $ie.link(:text, 'Profile'), :text => 'edit').click
$ie.link(:text, 'Picture').click
file = (File.dirname(File.expand_path(__FILE__)) + '\\sausheong.jpg').gsub! '/', '\\'
$ie.file_field(:id, 'pic').set(file)
$ie.checkbox(:id, 'agree').set
$ie.button(:id, 'uploadbutton').click
$ie.text.should include('Your picture has been uploaded successfully.')
end

end

Notice that we added in a do .. end block at the end of the expectation. The code is quite obvious in most parts. The first line in the first expectation block is:


$ie.link(:after? => $ie.link(:text, 'Profile'), :text => 'edit').click

It shouldn’t surprise you that it works exactly how it looks like. This tells IE to look for a link that is after the link that contains the text ‘Profile’, as well as has the text ‘edit’, then click that link.

The last line in the first block uses RSpec syntax again and is very ‘English’-like:


$ie.text.should include('Changes saved.')

This line tells us that the text in the page should have the words ‘Changes saved.’. If it doesn’t then this example fails.

The second expectation block uploads a file to Facebook. The code is again quite self-evident, we just need to set the full path to the file to upload to the upload file field and click on the upload button, then check if it is successful.

The second example is slightly more tricky, because we will be dealing with Javascript quite a bit.

describe '(Navigate online friends)' do

it 'should flash all online friends ' do
$ie.div(:id, 'buddy_list_tab').click
sleep 5
friend_items = $ie.lis.find_all {|li| li.id =~ /buddy_list_item_*/}
friend_items.each {|item| $ie.link(:after?, item).flash 5 }
$first_friend = friend_items.first
end

it 'should send the first friend a message' do
$ie.link(:after?, $first_friend).click
friend_id = $first_friend.id.delete('buddy_list_item_')
$ie.text_field(:id, "chat_input_#{friend_id}").set('Hello Facebook World!')
$ie.fire_keydown_on("chat_input_#{friend_id}", 13)
$ie.div(:id, "chat_conv_content_#{friend_id}").text.should include('Hello Facebook World!')
end

end

The first expectation block looks for the online friends tab at the bottom of Facebook, which is a

tag and click on it. It’s not a button but Watir is intelligent enough to trigger the onclick event that is attached to this tag. This shows the list of friends who are online. Note that we ask the script to wait for 5 seconds using the sleep method, this is because sometimes it takes some time for Facebook to look for friends who are online. Of course, we could have waited until there are some online friends but to make the code simpler I opted to just wait till there are some friends online. The next few lines simply gets the list of friends and flash each of them. Flashing (this is part of Watir) is the act of highlighting an element with a particularly bright color (defaults to yellow) a few times in succession.

The second expectation block takes the first friend it found in the previous block and clicks on the link. This should open up a chat window, and we will use that chat window and write some message in the input. The next bit is a bit tricky so I’ll go in depth a bit.

Watir has a method on any HTML element that fires events on it, like ‘onclick’, ‘onchange’ and so on. However this is a generic event firing method, which accepts only the event name as the input and nothing else. This is not useful in the case where we need to fire events and send in extra data, like when we type in data into an input field and press ‘enter’. Unfortunately this is precisely what is needed. If we use Watir’s fire_event method on the ‘onkeydown’ event, we won’t be able to send the chat message as it expects the ‘enter’ key to be pressed. This is because fire_event just tells IE that a keydown event happens but not what key was pressed.

So what can we do?

In comes some Ruby magic (well, object-oriented magic really), which allows us to extend the default Watir::IE class. Just add this class definition in the same file:


class Watir::IE
def fire_keydown_on(element_id, key)
ie.Document.parentWindow.execScript("key = document.createEventObject(); key.keyCode = #{key}")
ie.Document.parentWindow.execScript("document.getElementById('#{element_id}').fireEvent('onkeydown', key)")
end
end

We have just added a new method ‘fire_keydown_on’ for the IE class, which takes in the element id, and the key. This method calls Javascript to create an event object (this only works in IE, by the way) and sets the key code to be ’13′, which is the carriage-return key (the enter key). The it calls Javascript to get the HTML element (using the element id) and fire the ‘onkeydown’ event on it, while passing on the event object it just created.

Now armed with this new method, after typing in the chat message, we call keydown on the input field:


$ie.fire_keydown_on("chat_input_#{friend_id}", 13)

This sends an onkeydown event to that element and passes the parameter value that represents a carriage-return. This simulates the user actually pressing the ‘enter’ key and viola! the message is sent.

Let’s put everything in a single code listing:


require 'rubygems'
require 'spec'
require 'watir'
include Watir

email = 'YOUR FACEBOOK LOGIN EMAIL'
password = 'YOUR FACEBOOK PASSWORD'

describe 'Facebook' do
before :all do
$ie = IE.new
$ie.goto('http://www.facebook.com')
$ie.text_field(:id, 'email').set(email)
$ie.text_field(:id, 'pass').set(password)
$ie.button(:id, 'doquicklogin').click
end

describe '(Editing my profile)' do

it 'should add new work information' do
# add new work history information into the profile
$ie.link(:after? => $ie.link(:text, 'Profile'), :text => 'edit').click
$ie.link(:text, 'Work').click
$ie.text_field(:name, 'work_history_1_company').set('Way Cool Company')
$ie.text_field(:name, 'work_history_1_position').set('Master of the Universe')
$ie.text_field(:name, 'work_history_1_description').set('Lord and Sovereign of All I See')
$ie.text_field(:name, 'work_history_1_location_sq').set('Singapore')
$ie.checkbox(:name, 'work_history_1_workspan_current').set
$ie.select_list(:name, 'work_history_1_workspan_start_month').set('June')
$ie.select_list(:name, 'work_history_1_workspan_start_year').set('2008')
$ie.button(:id, 'save').click
# test to see if the changes are saved
$ie.text.should include('Changes saved.')
end

it 'should upload a new picture' do
# upload a picture from the local machine
$ie.link(:after? => $ie.link(:text, 'Profile'), :text => 'edit').click
$ie.link(:text, 'Picture').click
file = (File.dirname(File.expand_path(__FILE__)) + '\\sausheong.jpg').gsub! '/', '\\'
$ie.file_field(:id, 'pic').set(file)
$ie.checkbox(:id, 'agree').set
$ie.button(:id, 'uploadbutton').click
# test to see if the picture is uploaded
$ie.text.should include('Your picture has been uploaded successfully.')
end

end

describe '(Navigate online friends)' do

it 'should flash all online friends ' do
# click on the online friends tab to open the list
$ie.div(:id, 'buddy_list_tab').click
sleep 5
# flash every friend
friend_items = $ie.lis.find_all {|li| li.id =~ /buddy_list_item_*/}
friend_items.each {|item| $ie.link(:after?, item).flash 2}
$first_friend = friend_items.first
end

it 'should send the first friend a message' do
# send a message to the first friend
$ie.link(:after?, $first_friend).click

# -- first find the friend's Facebook ID
friend_id = $first_friend.id.delete('buddy_list_item_')
# -- then enter something into the chat input
$ie.text_field(:id, "chat_input_#{friend_id}").set('Hello Facebook World!')
# -- run some javascript to send an 'enter' keystroke to the chat input (this works only in IE)
$ie.fire_keydown_on("chat_input_#{friend_id}", 13)

# test to make sure the message is sent
$ie.div(:id, "chat_conv_content_#{friend_id}").text.should include('Hello Facebook World!')
end

end

after :all do
$ie.link(:text, 'logout').click
$ie.close
end
end

class Watir::IE
def fire_keydown_on(element_id, key)
ie.Document.parentWindow.execScript("key = document.createEventObject(); key.keyCode = #{key}")
ie.Document.parentWindow.execScript("document.getElementById('#{element_id}').fireEvent('onkeydown', key)")
end
end

Remember if you’re copying the code above directly, replace the image file name with something in the same directory as your ruby script.

When it comes down to actual development you’ll probably not get away with writing unit tests to make sure your code is good to go and RSpec is a very good framework to use for this. However for functional testing that really tests the features of your application from the viewpoint of the user of your web application, you should try out RSpec + Watir. It will give you new appreciation of how someone will navigate through your application and who knows, maybe to give you new ideas on improving the UI.

About these ads
Tagged with: , ,

20 Responses

Subscribe to comments with RSS.

  1. abhishek said, on October 22, 2009 at 10:34 pm

    Hi,

    I really liked Rspec.

    I am using SCite tool to execute Ruby scripts.
    Can you please tell how to execute Rspec using SciTE tool.
    I know how to execute from command line but really liked to execute form SciTE.

    • shelaine said, on July 26, 2011 at 1:51 pm

      HI,
      I want to know somethind the problem which you have asked,now I also want to use the rspec and watir ,but there have some problem,could you please tell me something about how to use rspec to handle the auto testing~

      thanks~

  2. robt said, on October 24, 2009 at 6:07 am

    Hi, abhishek. I did some research because I too would like to see RSpec run in SciTE and this seems to work:

    1) Go to Scite->Options->Open ruby.properties
    2) In the ruby.properties file add this (Lines starting with “#” are comments)
    so they are optional.

    #Text displayed in the SciTe Tools menu option. The “1″ indicates the “CTRL-#”
    # shortcut key in the menu.
    command.name.1.*.rb=RSpec
    #Command that executes when the options above runs. The number “1″ should
    # match whatever
    # number you use above
    command.1.*.rb=ruby.exe “c:\ruby\bin\spec” -fs $(FilePath)
    #Required for Windows. The “=0″ tells it to run in Scite Output. “=1″ will
    # put output in SciTe Output but will launch a Ruby cmd.exe. “=2″ will run the
    # RSpec command & output in a Windows shell rather than
    # the SciTE output. See your SciTe help (likely in c:\ruby\scite\SciTEDoc.html)
    command.subsystem.1.*.rb=0

    3) You shouldn’t have to open SciTE again but you might need to for the new
    properties to load.
    4) Run your script with your describe() and it() calls using your new Tools->RSpec
    option (or hit “Ctrl-#”). You should see all your output in SciTE

    I just figured it out today and it looks cool … Let me know how it works for you!

    • abhishek said, on October 26, 2009 at 9:10 pm

      Hi robt,

      Sorry for the late reply.(I thought i will get an email when someone post here)
      I have tried your changes in Ruby property file.
      But i will get this error if i run from SCITE(I will not get any error if i run from cmd prompt)

      OUTPUT:
      >ruby.exe “c:\ruby\bin\spec” -fs C:\Documents and Settings\abhishek.sreepal\Desktop\firwatir.rb
      ruby.exe: Invalid argument — “c:/ruby/bin/spec” (LoadError)
      >Exit code: 1

  3. robt said, on October 26, 2009 at 10:27 pm

    Not sure what that could be exactly. I tried a few things to replicate it but don’t get the exact same “Invalid argument” error. Make sure you have a file called “spec” in c:\ruby\bin. Depending on how you loaded ruby it might be in a different location on your drive and you may just need to change the path from “c:\ruby\bin\spec” to another value. Also, I get an error when the path has spaces. I tried a few things
    to get around it but no luck so I’ll have to play with it as I have time but you might also try moving your ruby file “firwatir.rb” to a directory other than “Documents and Settings” so the path to the file does not include a folder with spaces. Then run it again.

  4. abhishek said, on October 26, 2009 at 11:44 pm

    Hi robt,

    I have a file called “spec ” and “spec.bat” in c:\ruby\bin.

    I have added these line in my ruby.properties file

    command.name.1.*.rb=RSpec
    command.1.*.rb=ruby.exe “c:\ruby\bin\spec” -fs $(FilePath)
    command.subsystem.1.*.rb=0

    “Can You please specify What is the File Path”

    I have moved my file “firwatir.rb” to C:\

    Now when I execute(Ctrl+1), I still get the same error.

  5. robt said, on October 27, 2009 at 12:29 am

    The $(FilePath) is a variable SciTE uses to hold the path and name of the file you are running (in your case, on this try, it sounds like it would be “C:\firwatir.rb”).

    “spec” should be the only file you need to worry about.

    I copied and pasted the actual lines from this website in to my ruby.properties file in SciTE and then I got the same error you are seeing. It appears copy/paste is adding hidden extended-ASCII characters but, when I pull it up in a hex editor, I can’t seem to find and delete them all. It might be converting characters in to Unicode or something SciTE can’t translate but I don’t have time to dig in to it that deeply.

    So, instead, delete all the lines (the entire line) you copied and pasted from ruby.properties or restore your back up and start over. I **manually typed** these same lines in to my ruby.properties file and saved it and it worked fine. You’ll probably have to close SciTE and reopen it again and open your c:\firwatir.rb file to run your RSpec menu option again. That addressed the issue. Again, don’t copy and paste, delete every line previously added when you copied and pasted from this site and hand type them just like below.

    command.name.1.*.rb=RSpec
    command.1.*.rb=ruby.exe “c:\ruby\bin\spec” -fs $(FilePath)
    command.subsystem.1.*.rb=0

    Once you type that in, try running your “c:\firwatir.rb” file. If that runs, then surround $(FilePath) in quotes and you should be able to run “c:\Documents and Settings\firwatir.rb” like you first attempted. I discovered I had another issue when I tried that out but fixed it and it works. Thus, your second line above will look like this (note only “$(FilePath)” has changed):

    command.1.*.rb=ruby.exe “c:\ruby\bin\spec” -fs “$(FilePath)”

  6. abhishek said, on October 27, 2009 at 1:10 am

    Hi robt,

    Thanks for the reply.
    This time I manually edited the file “ruby.properties”
    I added and saved
    command.name.1.*.rb=RSpec
    command.1.*.rb=ruby.exe “c:\ruby\bin\spec” -fs $(FilePath)
    command.subsystem.1.*.rb=0

    Now when I run,

    I am getting following error

    >ruby.exe “c:\ruby\bin\spec” -fs C:\google_test_spec.rb
    ruby.exe: No such file or directory — c:/ruby/bin/spec (LoadError)
    >Exit code: 1

  7. robt said, on October 27, 2009 at 1:48 am

    The only way I can duplicate that is if I rename my “c:\ruby\bin\spec” file to something else and try running the new RSpec option in SciTE again. I suspect you don’t really have a file named “spec” in the folder “c:\ruby\bin”?

    If you do, see if your “spec” file is about 393 bytes in size and looks like this:

    —————————————————

    #!c:/ruby/bin/ruby.exe
    #
    # This file was generated by RubyGems.
    #
    # The application ‘rspec’ is installed as part of a gem, and
    # this file is here to facilitate running it.
    #

    require ‘rubygems’

    version = “>= 0″

    if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then
    version = $1
    ARGV.shift
    end

    gem ‘rspec’, version
    load Gem.bin_path(‘rspec’, ‘spec’, version)

    —————————————-

    If it’s not in c:\ruby\bin, use windows search to try and track down the file (it is named just “spec” with no extension unless you or something renamed it although I don’t know what would). You might simply need to change your path from “c:\ruby\bin\spec” to whatever the location is on your PC.

  8. abhishek said, on October 27, 2009 at 1:57 am

    Hi,

    I am sorry.
    This time i was editing in my home system.(I had ruby in C:\ in office where as ruby is in D drive in my home)
    So I changed the path to “d:\ruby\bin\spec”
    Its working. Thank You.

  9. abhishek said, on October 27, 2009 at 2:04 am

    Hi,

    I am using two framework(Rspec)
    Recently i came to know about cucumber which was great.
    I know i am asking for too much. :)
    But really liked to know whether cucumber will work in SciTE?
    Is there any way to run Cucumber in SciTE ?

  10. robt said, on October 27, 2009 at 2:06 am

    Cool! Easy enough to do. Glad it’s working for you. I figured it was something simple after figuring out the first problem. That copy/paste thing was odd though. I’ll have to look in to that later. Hopefully, you’ll find it valuable. It’s nice having RSpec show up in SciTE output for me and one of my co-workers likes it too.

  11. robt said, on October 27, 2009 at 3:02 am

    I only know what I know about Cucumber from what I’ve read in the last 15 minutes so this will give you a start but you will have to try it and research from there to see if you can get it to work.

    To add another menu option, just take what you added earlier and change the “1″ to “2″ and “RSpec” to “Cucumber” for “Cucumber Ctrl+2″ menu option in SciTE.
    Then it appears Cucumber uses files with a “.feature” extension so replace “rb” with “feature” and change the second line with the “$(FilePath)” in it to what you see below. Remember, don’t copy and paste it from this web page. Type it in or you are probably safe copying and pasting the original RSpec stuff in ruby.properties and editing from there.

    command.name.2.*.feature=Cucumber
    command.2.*.feature=ruby.exe “c:\ruby\bin\cucumber” “$(FilePath)”
    command.subsystem.2.*.feature=0

    This might help you see the concept of adding a menu option by incrementing the number, adding the proper file extension for the type of file that you want to run in SciTE and finding out what command line runs the code in question.

    See if this gets you started at least but I won’t be much more help for now only because we’re not using Cucumber so it’ll take me time to learn it so I don’t know much more on cucumber than you see here so I can’t tell you if it is working the way you need it.

    Start here and then just experiment with your ruby.properties file from there.

  12. Burton Daner said, on April 13, 2010 at 2:30 pm

    He is really so cute!

  13. Stephen Ponce said, on June 10, 2010 at 1:25 am

    This was a great article, I’m looking to incorporate Watir and Rspec for conducting User Acceptance Testing on an upcoming web release. My client will be very surprised when they see these test scripts (they’ve always done manual UAT – what a nightmare!!). Watir is a great tool, and with Rspec anyone can read through the test scripts and actually understand what is being tested.

    Thanks again for writing this post, it was extremely helpful! :)

    -stephen

  14. twbrtestinge said, on August 14, 2011 at 11:21 pm

    Good article. Thanks
    Another step by step approach to learn automation using ruby, rspec and watir http://testingandagile.blogspot.com/2011/08/getting-started-with-automation.html

  15. [...] the post was very inspired by KK post and Saush. Like this:LikeBe the first to like this [...]

  16. Girish Baldania said, on December 15, 2011 at 5:15 am

    Very informative post Saush….Thanks for your guidance to run Cucumber & Rspec scripts from SciTE.

    Is there a way we can debug the scripts in SciTE ??

    Thanks
    Girish

  17. Floyd said, on January 31, 2013 at 9:21 am

    Good day! This post could not be written any better! Reading this post reminds
    me of my previous room mate! He always kept chatting about this.
    I will forward this page to him. Pretty sure he will have
    a good read. Many thanks for sharing!

  18. Madelaine said, on May 23, 2013 at 9:00 am

    I’m not sure exactly why but this web site is loading incredibly slow for me. Is anyone else having this issue or is it a issue on my end? I’ll check back later on
    and see if the problem still exists.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 444 other followers

%d bloggers like this: